I'm trying to build a function that takes a string as input representing a time variable with no separators. The fraction part could have variable precision.
This is the code I came up with :
CREATE FUNCTION [dbo].[Convert_FullStringToTime]
(
-- Add the parameters for the function here
#inputString VARCHAR(17),
#fractionsPrecision INT = 0
)
RETURNS TIME(7)
AS
BEGIN
-- Declare the return variable here
DECLARE #Result TIME(7)
-- Add the T-SQL statements to compute the return value here
SELECT #Result =
TIMEFROMPARTS(
LEFT(#inputString, 2), --hh
SUBSTRING(#inputString, 3, 2), --mm
SUBSTRING(#inputString, 5, 2), --ss
RIGHT(#inputString, #fractionsPrecision), --ff
#fractionsPrecision
)
-- Return the result of the function
RETURN #Result
END
But I'm getting an error:
Scale argument is not valid. Valid expressions for data type time scale argument are integer constants and integer constant expressions.
Do I have to understand I really have to write a constant for the precision parameter ???
Why on earth has this been done that way ?
Is there a better solution than using a case statement to overcome this ridiculous problem ?
The error is actually telling you that what you want to do is explicitly not allowed. The precision parameter for TIMEFORPARTS must be a literal; it cannot be an expression, a column value or a variable. For your FUNCTION there is, in truth, little/no point in having a "variable" precision value either; a FUNCTION must also have an explicit data type defined and that include the length/scale/precision. You initially had your FUNCTION defined as a time, which is a synonym for time(7), and you have now change the FUNCTION to demonstrate that. As such it wouldn't matter what value the precision was passed, your FUNCTION would always return a time(7).
To address your latter comments:
Why on earth has this been done that way ?
Is there a better solution than using a case statement to overcome this ridiculous problem ?
I'm going to address your second comment first; it isn't a "ridiculous problem", the real problem is you want a value that doesn't have a strict definition. T-SQL is a declarative and compiled langauge; you need to explicity define what data types a value is. The fact you want a variable precision value is a strong indication of an XY Problem, however, what that problem is I don't know. There likely is a "better solution" to what you actually want to do, but without knowing what that is, we can't access that question here.
As for why it's done that way, I come back to the point of that the language is declarative and compiled; as such if you could pass a variable/expression then SQL Sevrer would know what data type to compile a column as. Take the following very simple statement:
SELECT TIMEFROMPARTS(0,0,0,0,V.I) AS T
FROM (VALUES(5),(6),(7))V(I)
Here the column V.I being (trying to be) used to define the precision for TIMEFROMPARTS. The problem though is that V.I has 3 different values, 5, 6, and 7, so what is the data type for the column defined as T? Should it be 7? 5? SQL Server doesn't know, because at the time it compiles the statement it cannot make an informed decision; for a table (rather than a VALUES table construct) the compiler wouldn't read the table first to then make a decision, that would be awful for performance.
As such you must use a literal to tell SQL Server what precision you need. Just like when you define a variable, you can't use the syntax varchar(#Len) or when you CONVERT a column you can't use the syntax CONVERT(decimal(T.P,T.S),MyColumn).
This the combination of function I came up with, taking into account the precision inherent to the TIME datatype. (Thanks to #Larnu)
First, the padding function (which SQL SERVER also lacks...)
CREATE FUNCTION [dbo].[Rpad]
(
-- Add the parameters for the function here
#inputString VARCHAR(MAX),
#paddingCharacter VARCHAR(1),
#paddingLength INT
)
RETURNS VARCHAR(MAX)
AS
BEGIN
-- Declare the return variable here
DECLARE #Result VARCHAR(MAX)
-- Add the T-SQL statements to compute the return value here
SELECT #Result =
LEFT(#inputString + REPLICATE(#paddingCharacter, #paddingLength), #paddingLength)
-- Return the result of the function
RETURN #Result
END
GO
Then the Time conversion function, which will always return a 7 precision TIME data type.
CREATE FUNCTION [dbo].[Convert_FullStringToTime]
(
-- Add the parameters for the function here
#inputString VARCHAR(13)
)
RETURNS TIME(7)
AS
BEGIN
-- Declare the return variable here
DECLARE #Result TIME(7)
-- Add the T-SQL statements to compute the return value here
SELECT #Result =
TIMEFROMPARTS(
LEFT(#inputString, 2), --hh
SUBSTRING(#inputString, 3, 2), --mm
SUBSTRING(#inputString, 5, 2), --ss
RIGHT([dbo].RPAD(#inputString, '0', 7), 7), --ff
7
)
-- Return the result of the function
RETURN #Result
END
Related
In TSQL, I need to format a string in a predefined format.
For eg:
SNO
STRING
FORMAT
OUTPUT
1
A5233GFCOP
*XXXXX-XXXXX
*A5233-GFCOP
2
K92374
/X-000XXXXX
/K-00092374
3
H91543987
XXXXXXXXX
H91543987
I am trying with FORMATMESSAGE() built in function.
For ex:
FORMATMESSAGE('*%s-%s','A5233','GFCOP')
FORMATMESSAGE('/%s-000%s','K','92374')
FORMATMESSAGE('%s','H91543987')
I am able to get the first argument by replace function but issue is second/third/fourth/.. arguments.
I don't know how to count respective X's between the various delimiters, so that I can use substring to pass in second/third/.. arguments. If I can count the respective # of X's from the Format column, I feel using substring we can get it but not sure how to count the respective X's.
Please let me know how to get through it or if there is any other simple approach.
Appreciate your help.
Thanks!
It's in theory quite simple, could probably be done set-based using string_split however that's not ideal as the ordering is not guaranteed. As the strings are fairly short then a scalar function should suffice. I don't think it can use function in-lining.
The logic is very simple, create a counter for each string, loop 1 character at a time and pull a character from one or the other into the output depending on if the format string is an X or not.
create or alter function dbo.fnFormatString(#string varchar(20), #format varchar(20))
returns varchar(20)
as
begin
declare #scount int=1, #fcount int=1, #slen int=len(#string), #flen int=Len(#format), #output varchar(20)=''
while #scount<=#slen or #fcount<=#slen
begin
if Substring(#format,#fcount,1)='X'
begin
set #output+=Substring(#string,#scount,1)
select #scount+=1, #fcount +=1
end
else
begin
set #output+=Substring(#format,#fcount,1)
set #fcount +=1
end
end
return #output
end;
select *, dbo.fnFormatString(string, [format])
from t
See working Fiddle
I get the error "Operand data type varchar is invalid for subtract operator." in line 6 where i set the dummy variable.
When i remove the first part there is no error, so i think the setting of the variable also tries to execute some of the functions in the string.
Is is possible to set a variable with functions without having the setting also trying to execute the functions? Or should this be done in an entirely different way?
All i want is the option to use dbo.TimeInterval(7) (or something like this) instead of "REPLACE([US-Date],'-','') between CONVERT(int,CONVERT(varchar(10),GETDATE()-7,112)) and CONVERT(int,CONVERT(varchar(10),GETDATE(),112))" when i want the time interval of 7 days from today...
CREATE FUNCTION dbo.TimeInterval(#Input VARCHAR(1000))
RETURNS VARCHAR(1000)
AS BEGIN
DECLARE #dummy VARCHAR(1000)
SET #dummy = 'REPLACE([US-Date],'-','') between CONVERT(int,CONVERT(varchar(10),GETDATE()-Tidsinterval,112)) and CONVERT(int,CONVERT(varchar(10),GETDATE(),112))'
SET #dummy = REPLACE(#dummy, 'Tidsinterval',#Input)
RETURN #dummy
END
DECLARE #TAX VARCHAR(30)=120.45
DECLARE #TaxRoundOf VARCHAR(30)
SET #TaxRoundOf=ROUND(#TAX,1)
SELECT #TaxRoundOf
This Gives Result (#TaxRoundOf=120.5)
DECLARE #TAX VARCHAR(30)=146.45
DECLARE #TaxRoundOf VARCHAR(30)
SET #TaxRoundOf=ROUND(#TAX,1)
SELECT #TaxRoundOf
This Gives Result (#TaxRoundOf=146.4)
But I need to return 146.50 . why this mismatch between two results?
any one can help plz?
Since you are using VARCHAR to store your numbers, SQL Server is having to do implicit conversion to float behind the scenes, which is having knock on effects on your calculations. You can reproduce this using the below query:
SELECT ROUND(CONVERT(FLOAT, 120.45),1), -- 120.5
ROUND(CONVERT(FLOAT, 146.45),1), -- 146.4
ROUND(CONVERT(DECIMAL(10, 2), 120.45),1), -- 120.50
ROUND(CONVERT(DECIMAL(10, 2), 146.45),1) -- 146.50
Since floating point numbers are not exact, 146.45 cannot be exactly represented as a float, and ends up being stored as a very slightly smaller number, so when this is passed to the round function, it is rounded down, instead of up.
The solution, as demonstrated by the 3rd and 4th columns in the above query, is to use a more precise data type.
You can use this:
SET #TaxRoundOf=ROUND(10 * CAST(#TAX AS FLOAT)) / 10
instead of:
SET #TaxRoundOf=ROUND(#TAX,1)
DEMO
PS as #GarethD already mentioned I wouldn't use #TAX as VARCHAR type.
You can also rely on numeric rounding instead of converting your string to a float, which can lose information.
Cast a string to numeric and then round:
select round(cast('146.45' as numeric(18,2)), 1)
-- 146.50
A decimal constant is already a decimal so there's no need to cast it:
select round(146.45, 1)
-- 146.50
Today I was testing something at work place and came across this one
Case 1:
Declare #a nvarchar(20)
Set #a = null
Select IsNull(LTrim(RTrim(Lower(#a))), -1)
Case 2:
Select IsNull(LTrim(RTrim(Lower(null))), -1)
The result in case 1 is -1 but * in case 2
I was expecting same results in both cases. Any reason?
Without the declaration of data type, null in this case is declared as varchar(1). You can observe this by selecting the results into a #temp table:
Select IsNull(LTrim(RTrim(Lower(null))), -1) as x INTO #x;
EXEC tempdb..sp_help '#x';
Among the results you'll see:
Column_name Type Length
----------- ------- ------
x varchar 1
Since -1 can't fit in a varchar(1), you are getting * as output. This is similar to:
SELECT CONVERT(VARCHAR(1), -1);
If you want to collapse to a string, then I suggest enclosing the integer in single quotes so there is no confusion caused by integer <-> string conversions that aren't intended:
SELECT CONVERT(VARCHAR(1), '-1'); -- yields "-"
SELECT CONVERT(VARCHAR(30), '-1'); -- yields "-1"
I would not make any assumptions about how SQL Server will handle a "value" explicitly provided as null, especially when complex expressions make it difficult to predict which evaluation rules might trump data type precedence.
In SQL Server, there are "typed NULLs" and "untyped NULLs".
In the first case, the NULL is typed—it is aware that NULL is a varchar(20) and so as your functions wrap the inner value, that data type is propagated throughout the expression.
In the second case, the NULL is untyped, so it has to infer the NULL's type from the surrounding expressions. The IsNull function evaluates the data type of the first operand and applies that to the whole expression, and thus the NULL defaults to varchar(1):
PRINT sql_variant_property(IsNull(LTrim(NULL), -1), 'BaseType'); -- varchar
PRINT sql_variant_property(IsNull(LTrim(NULL), -1), 'MaxLength'); -- 1
Another complication is that IsNull does not do type promotion in the same way that Coalesce does (though Coalesce has its own problems due to not being a function—it is expanded to a CASE expression, sometimes causing unexpected side-effects due to repeat expression evaluation). Look:
SELECT Coalesce(LTrim(NULL), -1);
This results in -1 with data type int!
Check out Sql Server Data Type Precedence and you'll see that int is much higher than varchar, so the whole expression becomes int.
The naked NULL is being passed to LOWER(), which expects a character. This is being defaulted to one character wide. The value "-1" doesn't fit in this field, so it is returning "*".
You can get the same effect with:
select isnull(CAST(NULL as varchar(1)), -1)
The following code also causes the problem:
declare #val varchar;
set #val = -1
select #val
Note that COALESCE() does not cause this problem.
I'm pretty sure this is fully documented behavior.
I've got a stored procedure I use to insert data from a csv. The data itself is a mix of types, some test, some dates, and some money fields. I need to guarantee that this data gets saved, even if it's formatted wrong, so, I'm saving them all to varchars. Later, once the data's been validated and checked off on, it will be moved to another table with proper datatypes.
When I do the insert into the first table, I'd like to do a check that sets a flag (bit column) on the row if it needs attention. For instance, if what should be a money number has letters in it, I need to flag that row and add the column name in an extra errormsg field I've got. I can then use that flag to find and highlight for the users in the interface the fields they need to edit.
The date parameters seem to be easy, I can just use IF ISDATE(#mydate) = '0' to test if that parameter could be converted from varchar to datetime. But, I can't seem to find an ISMONEY(), or anything that's remotely equivalent.
Does anyone know what to call to test if the contents of a varchar can legitimately be converted to money?
EDIT:
I haven't tested it yet, but what do you think of a function like this?:
CREATE FUNCTION CheckIsMoney
(
#chkCol varchar(512)
)
RETURNS bit
AS
BEGIN
-- Declare the return variable here
DECLARE #retVal bit
SET #chkCol = REPLACE(#chkCol, '$', '');
SET #chkCol = REPLACE(#chkCol, ',', '');
IF (ISNUMERIC(#chkCOl + 'e0') = '1')
SET #retVal = '1'
ELSE
SET #retVal = '0'
RETURN #retVal
END
GO
Update
Just finished testing the above code, and it works!
money is decimal in effect, so you test this way
Don't use ISNUMERIC out of the box though: it's unreliable. Use this:
ISNUMERIC(MyCOl + 'e0')
Note, if you have 6 decimal places then it will be lost on conversion to money
Other question with more info why: How to determine the field value which can not convert to (decimal, float,int) in SQL Server
Edit:
Can do it in one line if you want
ISNUMERIC(REPLACE(REPLACE(#chkCOl, '$', ''), ',', '') + 'e0')