Different results in SQL vs. VBA using POWER function and VBA equivalent - sql

I'm hoping someone who is actually good at math can help me out with this. I get a different result in SQL than I do in VBA. In SQL, I have this function that calculates a payment. Variables for both VBA and SQL are:
Principal = 239762.05
Rate = 0.03 (aka 3%) ETA: in both cases, 0.03 is divided by 12, so really this is .0025
Period = 268
#principal / (power(1+#rate,#period)-1) * (#rate*power(1+#rate,#period))
In SQL this gives a value of 1228.76 (rounded)
In VBA, I do not have the POWER function. So I copied this public function from the internet:
Public Function Power(ByVal number As Double, ByVal exponent As Double) As Double
Power = number ^ exponent
End Function
And I am calling it in a sub here like so:
NewPI = PrinBal / (Power(1 + IntCalc, Term) - 1) * (IntCalc * Power(1 + IntCalc, Term))
but here, the answer I get is 1228.63 (rounded). Only 13 cents off!
I have tried lots of adjustments and either ended up with this same figure, or a much worse result. I am thinking it's some sort of Order of Operations mistake, but I'm not sure.
EDIT
I am adding this to possibly get to the bottom of the problem, which might be the data types in the SQL version. This is the full function
create function [dbo].[PMT] (#rate numeric(15,9), #periods smallint, #principal numeric(20,2) )
returns numeric(16,2)
as
begin
declare #pmt numeric (38,9)
select #pmt = #principal / (power(1+#rate,#periods)-1) * (#rate*power(1+#rate,#periods))
return #pmt
end

Short answer: Data types in SQL matter.
Worse, you can experience some implicit data conversion. Check this out...
DECLARE #RealPrincipal real
SET #RealPrincipal = 239762.05
DECLARE #RealRate real
SET #RealRate = 0.0025 --0.03 --(aka 3%) ETA: in both cases, 0.03 is divided by 12, so really this is .0025
DECLARE #Period int
SET #Period = 268
SELECT #RealPrincipal / (power(1.0+#RealRate,#Period)-1.0) * (#RealRate*power(1.0+#RealRate,#Period))
Result = 1228.61333410069
Compare that to your formula from your OP comment with all literals and no variables...
SELECT 239762.05 / (power(1+0.0025,268)-1) * (0.0025*power(1+0.0025,268)) as 'Value!'
Result = 1228.761629
Now, use the same exact structure as the first code block, but replace the real type variables with money type...
DECLARE #moneyPrincipal money
SET #moneyPrincipal = 239762.05
DECLARE #moneyRate money
SET #moneyRate = 0.0025 --(aka 3%) ETA: in both cases, 0.03 is divided by 12, so moneyly this is .0025
DECLARE #Period int
SET #Period = 268
SELECT #moneyPrincipal / (power(1+#moneyRate,#Period)-1) * (#moneyRate*power(1+#moneyRate,#Period))
Result = 1233.2921
Now, using the money data types, watch what happens when you replace the literal 1 values in the formula with 1.0 ...
SELECT #moneyPrincipal / (power(1.0+#moneyRate,#Period)-1.0) * (#moneyRate*power(1.0+#moneyRate,#Period))
Result = 1228.761629

This is a simply explanation. VBA is simply using a higher precision then SQL. Not saying this is best practice and i am sure someone else it going to SLAP / downvote me but if you change it too something like the below you will see it forces SQL to use more decimal places and you get the same answer.
select
#principal/(POWER((#rate+1)*1.000000000,#period*1.00000000000000000)-1)(#ratePOWER((#rate+1)*1.0000000000000000000,#period*1.00000000000000000))
Again there are better ways to do this, this is just the quick and dirty.

I think you should simplify your calculation:
In VBA:
function payment(principal as double, rate as double, term as integer) as double
' Inputs:
' principal: Ammount of the loan
' rate: Effective interest rate per period
' term: Number of periods / payments
dim v as double, a as double
v = 1 / (1 + rate)
a = (1 - (v ^ term) / rate)
payment = principal / a
end function
or, if you want it squeezed into a single line:
function payment(principal as double, rate as double, term as integer) as double
' Inputs:
' principal: Ammount of the loan
' rate: Effective interest rate per period
' term: Number of periods / payments
payment = principal * rate / (1 - ((1 + rate) ^ (-term)))
end function
and, in SQL:
#principal * #rate / (1 - power(1 + #rate, -#term))
-- #rate is the effective rate per period (in your example: 0.0025)
Minimize the number of arithmetic operations.
If you're having problems with data type (as LDMJoe mentions in his answer), you should try casting each variable to an appropriate data type. In Access you can do something like this (assuming you're writing the expression in a query):
CDbl(principal) * CDbl(rate) / (1 - power(1 + CDbl(rate), -CInt(term))
I'm also assuming that principal, rate and term are columns in a table.

Related

SQL order of operation does not make sense with simple mathematical calculation

I have a simple calculation to do in a stored procedure. Depending on the order I put the variables I get a different result
If you copy/past this into SQL Query Analyzer you can easily reproduce the issue where I get a different result. The result I was looking for was the second calculation (57364.32)
DECLARE #mnyDocTotal MONEY;
DECLARE #mnyUSDTotal MONEY
DECLARE #mnyDetailLine MONEY
SET #mnyDocTotal = 78000
SET #mnyUSDTotal = 86046.48
SET #mnyDetailLine = 52000
PRINT 'Result: ' + CAST(ROUND(#mnyDetailLine / #mnyDocTotal * #mnyUSDTotal,2) as char(20))
PRINT 'Result: ' + CAST(ROUND(#mnyDetailLine * #mnyUSDTotal / #mnyDocTotal,2) as char(20))
--Result: 57358.58
--Result: 57364.32
I believe that / and * are on the same level and operate from left to right in this case.
If you run the numbers with a calculator, you will always get 57364.32.
This caused me about 2 hours of effort to figure this out. In all my years I've never had this issue occur. Why is the result different?
This article does a pretty good job explaining why you should not use money.
They are not numeric values. They are stored as integers. And they have rounding problems. So:
(a * b) / c
can product a different result due to rounding from:
(a / c) * b
This is actually true of integers in general, as this simple example illustrates:
select (2 * 4 / 3), (2 / 3 * 4)
If you use numeric, you won't have a problem. Here is a db<>fiddle.

T-SQL - how to round DOWN to nearest .05

The database I am using is SQL Server 2005. I am trying to round values DOWN to the nearest .05 (nickel).
So far I have:
SELECT ROUND(numberToBeRounded / 5, 2) * 5
which almost works - what I need is for the expression, when numberToBeRounded is 1.99, to evaluate to 1.95, not 2.
Specify a non-zero value for a third parameter to truncate instead of round:
SELECT ROUND(numberToBeRounded / 5, 2, 1) * 5
Note: Truncating rounds toward zero, rather than down, but that only makes a difference if you have negative values. To round down even for negative values you can use the floor function, but then you can't specify number of decimals so you need to multiply instead of dividing:
SELECT FLOOR(numberToBeRounded * 20) / 20
If your data type is numeric (ISO decimal) or `money, you can round towards zero quite easily, to any particular "unit", thus:
declare #value money = 123.3499
declare #unit money = 0.05
select value = value ,
rounded_towards_zero = value - ( value % #unit )
from #foo
And it works regardless of the sign of the value itself, though the unit to which you're rounding should be positive.
123.3499 -> 123.3000
-123.3499 -> -123.3000

SQL Server Rounding Issue where there is 5

As far as i know according to mathematics rounding should work as below when rounding number is 5.
2.435 => 2.44 (Round Up, if rounding to digit(3) is odd number)
2.445 => 2.44 (Round Down, if rounding to digit(4) is even number)
if we do summation all fine,
2.435 + 2.445 = 4.88
2.44 + 2.44 = 4.88
I'm pretty sure in .Net also rounding works like this.
But in SQL server, 5 is always rounding up which is not correct according to maths.
SELECT round(2.345, 2) = 2.35
SELECT round(2.335, 2) => 2.34
this results to 1 cent discrepancies in summation of rounded values.
2.345 + 2.335 = 4.68
2.35 + 2.34 = 4.69 => which is not correct
I have tried this with decimal and money data types.
Am i doing something wrong? Is there a work around for this?
If you do want to use banker's rounding in SQL Server...
CREATE FUNCTION BankersRounding(#value decimal(36,11), #significantDigits INT)
RETURNS MONEY
AS
BEGIN
-- if value = 12.345 and signficantDigits = 2...
-- base = 1000
declare #base int = power(10, #significantDigits + 1)
-- roundingValue = 12345
declare #roundingValue decimal(36,11) = floor(abs(#value) * #base)
-- roundingDigit = 5
declare #roundingDigit int = #roundingValue % 10
-- significantValue = 1234
declare #significantValue decimal(36,11) = floor(#roundingValue / 10)
-- lastSignificantDigit = 4
declare #lastSignificantDigit int = #significantValue % 10
-- awayFromZero = 12.35
declare #awayFromZero money = (#significantValue + 1) / (#base / 10)
-- towardsZero = 12.34
declare #towardsZero money = #significantValue / (#base / 10)
-- negative values handled slightly different
if #value < 0
begin
-- awayFromZero = -12.35
set #awayFromZero = ((-1 * #significantValue) - 1) / (#base / 10)
-- towardsZero = -12.34
set #towardsZero = (-1 * #significantValue) / (#base / 10)
end
-- default to towards zero (i.e. assume thousandths digit is 0-4)
declare #rv money = #towardsZero
if #roundingDigit > 5
set #rv = #awayFromZero -- 5-9 goes away from 0
else if #roundingDigit = 5
begin
-- 5 goes to nearest even number (towards zero if even, away from zero if odd)
set #rv = case when #lastSignificantDigit % 2 = 0 then #towardsZero else #awayFromZero end
end
return #rv
end
You're looking for Banker's Rounding - which is the default rounding in C# but is not how SQL Server ROUND() works.
See Why does TSQL on Sql Server 2000 round decimals inconsistently? as well as http://blogs.lessthandot.com/index.php/DataMgmt/DataDesign/sql-server-rounding-methods and http://www.chrispoulter.com/blog/post/rounding-decimals-using-net-and-t-sql
Mathematically rounding up at 5 is correct, and also the most commonly used type of rounding in basic mathematics. Other types of rounding are also valid, but are not basic mathematics, but more often used in certain areas due to 0.5 often being a dispute number.
What you call mathematically rounding is actually bankers rounding, which is the type of rounding used in the finance business.

return datediff as decimal/percent

i am currently writing a scalar valued function and i'm having a few issue with the returned result.
i have narrowed the problem down to a calulation that convert the difference between two dates as a percentage/decimal. no matter what i try the return value is always a whole number
set #earnedpremium = (#premium * #pretripearnings) + ((#premium - (#premium * #pretripearnings)) * cast((datediff(day, #outdate, #experiencedate) / datediff(day, #outdate, #returndate))as decimal(5,2)))
the cast section needs to return the percentage, i know the rest is working fine through some elimination and testing.
can someone please help me figure out what im doing wrong??
It's because DATEDIFF returns an INTEGER and so you need to cast both parts of that operation to DECIMAL:
set #earnedpremium = (#premium * #pretripearnings) + ((#premium - (#premium * #pretripearnings))
* (CAST(datediff(day, #outdate, #experiencedate) AS DECIMAL(5,2)) /
CAST(datediff(day, #outdate, #returndate) AS DECIMAL(5,2)))

Round to n Significant Figures in SQL

I would like to be able to round a number to n significant figures in SQL. So:
123.456 rounded to 2sf would give 120
0.00123 rounded to 2sf would give 0.0012
I am aware of the ROUND() function, which rounds to n decimal places rather than significant figures.
select round(#number,#sf-1- floor(log10(abs(#number)))) should do the trick !
Successfully tested on your two examples.
Edit : Calling this function on #number=0 won't work. You should add a test for this before using this code.
create function sfround(#number float, #sf int) returns float as
begin
declare #r float
select #r = case when #number = 0 then 0 else round(#number ,#sf -1-floor(log10(abs(#number )))) end
return (#r)
end
Adapted the most popular answer by Brann to MySQL for those who come looking like me.
CREATE FUNCTION `sfround`(num FLOAT, sf INT) # creates the function
RETURNS float # defines output type
DETERMINISTIC # given input, will return same output
BEGIN
DECLARE r FLOAT; # make a variable called r, defined as a float
IF( num IS NULL OR num = 0) THEN # ensure the number exists, and isn't 0
SET r = num; # if it is; leave alone
ELSE
SET r = ROUND(num, sf - 1 - FLOOR(LOG10(ABS(num))));
/* see below*/
END IF;
RETURN (r);
END
/* Felt too long to put in comment */
ROUND(num, sf - 1 - FLOOR(LOG10(ABS(num))))
The part that does the work - uses ROUND function on the number as normal, but the length to be rounded to is calculated
ABS ensures positive
LOG10 gets the number of digits greater than 0 in the number
FLOOR gets the largest integer smaller than the resultant number
So always rounds down and gives an integer
sf - 1 - FLOOR(...) gives a negative number
works because ROUND(num, -ve num) rounds to the left of the decimal point
For just a one off, ROUND(123.456, -1) and ROUND(0.00123,4)
return the requested answers ((120, 0.0012)
I think I've managed it.
CREATE FUNCTION RoundSigFig(#Number float, #Figures int)
RETURNS float
AS
BEGIN
DECLARE #Answer float;
SET #Answer = (
SELECT
CASE WHEN intPower IS NULL THEN 0
ELSE FLOOR(fltNumber * POWER(CAST(10 AS float), intPower) + 0.5)
* POWER(CAST(10 AS float), -intPower)
END AS ans
FROM (
SELECT
#Number AS fltNumber,
CASE WHEN #Number > 0
THEN -((CEILING(LOG10(#Number)) - #Figures))
WHEN #Number < 0
THEN -((FLOOR(LOG10(#Number)) - #Figures))
ELSE NULL END AS intPower
) t
);
RETURN #Answer;
END
You could divide by 100 before rounding and then multiplying by 100...