Related
I've encountered an interesting SQL server behaviour while trying to generate random values in T-sql using RAND and CHOOSE functions.
My goal was to try to return one of two given values using RAND() as rng. Pretty easy right?
For those of you who don't know it, CHOOSE function accepts in an index number(int) along with a collection of values and returns a value at specified index. Pretty straightforward.
At first attempt my SQL looked like this:
select choose(ceiling((rand()*2)) ,'a','b')
To my surprise, this expression returned one of three values: null, 'a' or 'b'. Since I didn't expect the null value i started digging. RAND() function returns a float in range from 0(included) to 1 (excluded). Since I'm multiplying it by 2, it should return values anywhere in range from 0(included) to 2 (excluded). Therefore after use of CEILING function final value should be one of: 0,1,2. After realising that i extended the value list by 'c' to check whether that'd be perhaps returned. I also checked the docs page of CEILING and learnt that:
Return values have the same type as numeric_expression.
I assumed the CEILINGfunction returned int, but in this case would mean that the value is implicitly cast to int before being used in CHOOSE, which sure enough is stated on the docs page:
If the provided index value has a numeric data type other than int,
then the value is implicitly converted to an integer.
Just in case I added an explicit cast. My SQL query looks like this now:
select choose(cast(ceiling((rand()*2)) as int) ,'a','b','c')
However, the result set didn't change. To check which values cause the problem I tried generating the value beforehand and selecting it alongside the CHOOSE result. It looked like this:
declare #int int = cast(ceiling((rand()*2)) as int)
select #int,choose( #int,'a','b','c')
Interestingly enough, now the result set changed to (1,a), (2,b) which was my original goal. After delving deeper in the CHOOSE docs page and some testing i learned that 'null' is returned in one of two cases:
Given index is a null
Given index is out of range
In this case that would mean that index value when generated inside the SELECT statement is either 0 or above 2/3 (I'm assuming that negative numbers are not possible here and CHOOSE function indexes from 1). As I've stated before 0 should be one of possibilities of:
ceiling((rand()*2))
,but for some reason it's never 0 (at least when i tried it 1 million+ times like this)
set nocount on
declare #test table(ceiling_rand int)
declare #counter int = 0
while #counter<1000000
begin
insert into #test
select ceiling((rand()*2))
set #counter=#counter+1
end
select distinct ceiling_rand from #test
Therefore I assume that the value generated in SELECT is greater than 2/3 or NULL. Why would it be like this only when generated in SELECT statement? Perhaps order of resolving CAST, CELING or RAND inside SELECT is different than it would seem? It's true I've only tried it a limited number of times, but at this point the chances of it being a statistical fluctuation are extremely small. Is it somehow a floating-point error? I truly am stumbled and looking forward to any explanation.
TL;DR: When generating a random number inside a SELECT statement result set of possible values is different then when it's generated before the SELECT statement.
Cheers,
NFSU
EDIT: Formatting
You can see what's going on if you look at the execution plan.
SET SHOWPLAN_TEXT ON
GO
SELECT (select choose(ceiling((rand()*2)) ,'a','b'))
Returns
|--Constant Scan(VALUES:((CASE WHEN CONVERT_IMPLICIT(int,ceiling(rand()*(2.0000000000000000e+000)),0)=(1) THEN 'a' ELSE CASE WHEN CONVERT_IMPLICIT(int,ceiling(rand()*(2.0000000000000000e+000)),0)=(2) THEN 'b' ELSE NULL END END)))
The CHOOSE is expanded out to
SELECT CASE
WHEN ceiling(( rand() * 2 )) = 1 THEN 'a'
ELSE
CASE
WHEN ceiling(( rand() * 2 )) = 2 THEN 'b'
ELSE NULL
END
END
and rand() is referenced twice. Each evaluation can return a different result.
You will get the same problem with the below rewrite being expanded out too
SELECT CASE ceiling(( rand() * 2 ))
WHEN 1 THEN 'a'
WHEN 2 THEN 'b'
END
Avoid CASE for this and any of its variants.
One method would be
SELECT JSON_VALUE ( '["a", "b"]' , CONCAT('$[', FLOOR(rand()*2) ,']') )
I want to determine if a value is integer (like TryParse in .NET). Unfortunatelly ISNUMERIC does not fit me because I want to parse only integers and not every kind of number. Is there such thing as ISINT or something?
Here is some code to make things clear. If MY_FIELD is not int, this code would fail:
SELECT #MY_VAR = CAST(MY_FIELD AS INT)
FROM MY_TABLE
WHERE MY_OTHER_FIELD = 'MY_FILTER'
Thank you
Here's a blog post describing the creation of an IsInteger UDF.
Basically, it recommends adding '.e0' to the value and using IsNumeric. In this way, anything that already had a decimal point now has two decimal points, causing IsNumeric to be false, and anything already expressed in scientific notation is invalidated by the e0.
In his article Can I convert this string to an integer?, Itzik Ben-Gan provides a solution in pure T-SQL and another that uses the CLR.
Which solution should you choose?
Is the T-SQL or CLR Solution Better? The advantage of using the T-SQL
solution is that you don’t need to go outside the domain of T-SQL
programming. However, the CLR solution has two important advantages:
It's simpler and faster. When I tested both solutions against a table
that had 1,000,000 rows, the CLR solution took two seconds, rather
than seven seconds (for the T-SQL solution), to run on my laptop. So
the next time you need to check whether a given string can be
converted to an integer, you can include the T-SQL or CLR solution
that I provided in this article.
If you only want to maintain T-SQL, then use the pure T-SQL solution. If performance is more important than convenience, then use the CLR solution.
The pure T-SQL Solution is tricky. It combines the built-in ISNUMERIC function with pattern-matching and casting to check if the string represents an int.
SELECT keycol, string, ISNUMERIC(string) AS is_numeric,
CASE
WHEN ISNUMERIC(string) = 0 THEN 0
WHEN string LIKE '%[^-+ 0-9]%' THEN 0
WHEN CAST(string AS NUMERIC(38, 0))
NOT BETWEEN -2147483648. AND 2147483647. THEN 0
ELSE 1
END AS is_int
FROM dbo.T1;
The T-SQL part of the CLR solution is simpler. You call the fn_IsInt function just like you would call ISNUMERIC.
SELECT keycol, string, ISNUMERIC(string) AS is_numeric,
dbo.fn_IsInt(string) AS is_int
FROM dbo.T1;
The C# part is simply a wrapper for the .NET's parsing function Int32.TryParse. This works because the SQL Server int and the .NET Int32 are both 32-bit signed integers.
using System;
using System.Data.SqlTypes;
public partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlBoolean fn_IsInt(SqlString s)
{
if (s.IsNull)
return SqlBoolean.False;
else
{
Int32 i = 0;
return Int32.TryParse(s.Value, out i);
}
}
};
Please read Itzik's article for a full explanation of these code samples.
With sqlserver 2005 and later you can use regex-like character classes with LIKE operator. See here.
To check if a string is a non-negative integer (it is a sequence of decimal digits) you can test that it doesn't contain other characters.
SELECT numstr
FROM table
WHERE numstr NOT LIKE '%[^0-9]%'
Note1: This will return empty strings too.
Note2: Using LIKE '%[0-9]%' will return any string that contains at least a digit.
See fiddle
WHERE IsNumeric(MY_FIELD) = 1 AND CAST(MY_FIELD as VARCHAR(5)) NOT LIKE '%.%'
That is probably the simplest solution. Unless your MY_FIELD contains .00 or something of that sort. In which case, cast it to a float to remove any trailing .00s
Necromancing.
As of SQL-Server 2012+, you can use TRY_CAST, which returns NULL if the cast wasn't successful.
Example:
DECLARE #foo varchar(200)
SET #foo = '0123'
-- SET #foo = '-0123'
-- SET #foo = '+0123'
-- SET #foo = '+-0123'
-- SET #foo = '+-0123'
-- SET #foo = '.123'
-- SET #foo = '1.23'
-- SET #foo = '.'
-- SET #foo = '..'
-- SET #foo = '0123e10'
SELECT CASE WHEN TRY_CAST(#foo AS integer) IS NULL AND #foo IS NOT NULL THEN 0 ELSE 1 END AS isInteger
This is the only really reliable way.
Should you need support for SQL-Server 2008, then fall back to Sam DeHaan's answer:
SELECT CASE WHEN ISNUMERIC(#foo + '.e0') = 1 THEN 1 ELSE 0 END AS isInteger
SQL-Server < 2012 (aka 2008R2) will reach end of (extended) support by 2019-07-09.
At this time, which is very soon, support for < 2012 can be dropped.
I wouldn't use any of the other hacks at this point in time anymore.
Just tell your frugal customers to update - it's been over 10 years since 2008.
See whether the below query will help
SELECT *
FROM MY_TABLE
WHERE CHARINDEX('.',MY_FIELD) = 0 AND CHARINDEX(',',MY_FIELD) = 0
AND ISNUMERIC(MY_FIELD) = 1 AND CONVERT(FLOAT,MY_FIELD) / 2147483647 <= 1
The following is correct for a WHERE clause; to make a function wrap it in CASE WHEN.
ISNUMERIC(table.field) > 0 AND PATINDEX('%[^0123456789]%', table.field) = 0
This work around with IsNumeric function will work:
select * from A where ISNUMERIC(x) =1 and X not like '%.%'
or Use
select * from A where x **not like** '%[^0-9]%'
declare #i numeric(28,5) = 12.0001
if (#i/cast(#i as int) > 1)
begin
select 'this is not int'
end
else
begin
select 'this is int'
end
As of SQL Server 2012, the TRY_CONVERT and TRY_CAST functions were implemented. Thise are vast improvements over the ISNUMERIC solution, which can (and does) give false positives (or negatives). For example if you run the below:
SELECT CONVERT(int,V.S)
FROM (VALUES('1'),
('900'),
('hello'),
('12b'),
('1.1'),
('')) V(S)
WHERE ISNUMERIC(V.S) = 1;
Using TRY_CONVERT (or TRY_CAST) avoids that:
SELECT TRY_CONVERT(int,V.S),
V.S,
ISNUMERIC(V.S)
FROM (VALUES('1'),
('900'),
('hello'),
('12b'),
('1.1'),
('')) V(S)
--WHERE TRY_CONVERT(int,V.S) IS NOT NULL; --To filter to only convertable values
Notice that '1.1' returned NULL, which cause the error before (as a string represtation of a decimal cannot be converted to an int) but also that '' returned 0, even though ISNUMERIC states the value "can't be converted".
Use TRY_CONVERT which is an SQL alternative to TryParse in .NET. IsNumeric() isn’t aware that empty strings are counted as (integer)zero, and that some perfectly valid money symbols, by themselves, are not converted to (money)zero. reference
SELECT #MY_VAR = CASE WHEN TRY_CONVERT(INT,MY_FIELD) IS NOT NULL THEN MY_FIELD
ELSE 0
END
FROM MY_TABLE
WHERE MY_OTHER_FIELD = 'MY_FILTER'
I think that there is something wrong with your database design. I think it is a really bad idea to mix varchar and numbers in one column? What is the reason for that?
Of course you can check if there are any chars other than [0-9], but imagine you have a 1m rows in table and your are checking every row. I think it won't work well.
Anyway if you really want to do it I suggest doing it on the client side.
I have a feeling doing it this way is the work of satan, but as an alternative:
How about a TRY - CATCH?
DECLARE #Converted as INT
DECLARE #IsNumeric BIT
BEGIN TRY
SET #Converted = cast(#ValueToCheck as int)
SET #IsNumeric=1
END TRY
BEGIN CATCH
SET #IsNumeric=0
END CATCH
select IIF(#IsNumeric=1,'Integer','Not integer') as IsInteger
This works, though only in SQL Server 2008 and up.
I tried this script and got the answer
ISNUMERIC(Replace(Replace([enter_your_number],'+','A'),'-','A') + '.0e0')
for example for up question this is answer:
SELECT #MY_VAR = CAST(MY_FIELD AS INT)
FROM MY_TABLE
WHERE MY_OTHER_FIELD = 'MY_FILTER' and ISNUMERIC(Replace(Replace(MY_FIELD,'+','A'),'-','A') + '.0e0') = 1
Why not just do something like:
CASE
WHEN ROUND(MY_FIELD,0)=MY_FIELD THEN CAST(MY_FIELD AS INT)
ELSE MY_FIELD
END
as MY_FIELD2
Sometimes you don't get to design the database, you just have to work with what you are given. In my case it's a database located on a computer that I only have read access to which has been around since 2008.
I need to select from a column in a poorly designed database which is a varchar with numbers 1-100 but sometimes a random string. I used the following to get around it (although I wish I could have re designed the entire database).
SELECT A from TABLE where isnumeric(A)=1
I am not a Pro in SQL but what about checking if it is devideable by 1 ?
For me it does the job.
SELECT *
FROM table
WHERE fieldname % 1 = 0
Use PATINDEX
DECLARE #input VARCHAR(10)='102030.40'
SELECT PATINDEX('%[^0-9]%',RTRIM(LTRIM(#input))) AS IsNumber
reference
http://www.intellectsql.com/post-how-to-check-if-the-input-is-numeric/
Had the same question. I finally used
where ATTRIBUTE != round(ATTRIBUTE)
and it worked for me
WHERE IsNumeric(value + 'e0') = 1 AND CONVERT(FLOAT, value) BETWEEN -2147483648 AND 2147483647
Seeing as this is quite old, but my solution isn't here, i thought to add another possible way to do this:
--This query only returns values with decimals
SELECT ActualCost
FROM TransactionHistory
where cast(ActualCost as int) != ActualCost
--This query only returns values without decimals
SELECT ActualCost
FROM TransactionHistory
where cast(ActualCost as int) = ActualCost
The easy part here is checking if the selected value is the same when cast as an integer.
we can check if its a non integer by
SELECT number2
FROM table
WHERE number2 LIKE '%[^0-9]%' and (( right(number2 ,len(number2)-1) LIKE '%[^0-9]%' and lefT(number2 ,1) <> '-') or ( right(number2 ,len(number2)-1) LIKE '%[^0-9]%' and lefT(number2 ,1) in ( '-','+') ) )
DECLARE #zip_code NCHAR(10)
SET #zip_code = '1239'
IF TRY_PARSE( #zip_code AS INT) / TRY_PARSE( #zip_code AS INT) = 1 PRINT 'integer'
ELSE PRINT 'not integer'
This works fine in SQL Server
SELECT (SELECT ISNUMERIC(2) WHERE ISNUMERIC(2)=1 AND 2 NOT LIKE '%.%')
Case
When (LNSEQNBR / 16384)%1 = 0 then 1
else 0
end
I have some dirty input data that is being imported into a raw source table within SQL Server (2008 R2). Fields that are defined as decimal(9,2) or decimal(4,2) by the input provider are coming in as strings, however, the strings do not always conform to the data definition (go figure!).
We import the data from flat files into the raw tables,then apply some conversion scripts to insert the 'cleaned' data into tables with the proper data types assigned to columns.
For instance:
raw_table
TotalAmount varchar(12)
clean_table
TotalAmount decimal(9,2)
Now, my question is this. If I want to do some 'basic' cleanup on this, I would want to do it in a function along the lines of:
CREATE FUNCTION [dbo].[StringToDecimal]
(
#conversionString VARCHAR(12)
)
RETURNS DECIMAL(9,2)
AS
BEGIN
DECLARE #rsp DECIMAL(9,2)
IF ISNUMERIC( LTRIM(RTRIM(REPLACE(#conversionString,' ',''))) ) = 1
BEGIN
SET #rsp = ISNULL( CONVERT( decimal(17,6), NULLIF( LTRIM(RTRIM(REPLACE(#conversionString,' ',''))),'') ), 0 )
END
ELSE
BEGIN
SET #rsp = 0 -- or we can return NULL here
END
RETURN #rsp
END
However, how could one go about supporting various sized decimals in this mix? Is there a way to parametrize the response type? I considered just returning a decimal of the largest size we generally see, then converting it again on the other end, however, you run into arithmetic overflow issues.
Would appreciate any thoughts/insight into solving this one!
Is there a way to parametrize the response type?
It's simpler than you think. Just return as a VARCHAR and do the casting to decimal(x,y) from the VARCHAR. You don't even need to cast - you can directly assign a VARCHAR (as long as it holds valid decimal data) to a decimal column/variable.
I will create 2 functions instead. StringToDecimal2 does the actual conversion, but returns one of 6 "error codes". You can use it to check why a string is invalid. Or use the wrapper dbo.StringToDecimal which just turns the invalid codes into NULL.
CREATE FUNCTION [dbo].[StringToDecimal2]
(
#conversionString VARCHAR(12),
#precision int, -- total digits
#scale int -- after decimal point
)
RETURNS VARCHAR(100)
AS
BEGIN
-- remove spaces, we'll allow this error. no need to trim
set #conversionString = REPLACE(#conversionString,' ','')
-- note: 1,234.56 (thousands separated) will be invalid, so will 1,234,56 (European decimals)
-- well, ok, let's clean up the thousands separators. BUT! It will incorrectly scale European decimals
set #conversionString = REPLACE(#conversionString,',','')
-- we don't support scientific notation either, so 1e4 (10,000) is out
if #conversionString like '%[^0-9.+-]%' return 'INVALID1' -- only digits and decimal are valid (plus +-)
if #conversionString like '%.%.%' return 'INVALID2' -- too many decimals
if #conversionString like '_%[+-]%' return 'INVALID3' -- +- symbol not in the first position
if #conversionString like '[.+-]' return 'INVALID4' -- a single character from "+-."
if #conversionString like '[+-].' return 'INVALID5' -- symbol and decimal only
-- add a decimal place so it is easier to work with below
if #conversionString not like '%.%'
set #conversionString = #conversionString + '.'
-- allow decimal places to go only as far as scale
set #conversionString = left(#conversionString, charindex('.', #conversionString)+#scale)
-- ensure the data is within precision number of digits in total
if charindex('.', #conversionString) > #precision - #scale + 1
return 'INVALID6' -- too many digits before decimal
RETURN #conversionString
END
GO
CREATE FUNCTION [dbo].[StringToDecimal]
(
#conversionString VARCHAR(12),
#precision int, -- total digits
#scale int -- after decimal point
)
RETURNS VARCHAR(100)
AS
BEGIN
RETURN case when [dbo].[StringToDecimal2](#conversionString, #precision, #scale) like 'INVALID%'
then null else [dbo].[StringToDecimal2](#conversionString, #precision, #scale) end
END
GO
Some tests:
select [dbo].[StringToDecimal2]('12342342', 9,2)
select convert(decimal(9,2),[dbo].[StringToDecimal]('1234234', 9,2))
select convert(decimal(9,2),[dbo].[StringToDecimal]('12342342', 9,2))
select convert(decimal(9,2),[dbo].[StringToDecimal]('123423.3333', 9,2))
select convert(decimal(20,10),[dbo].[StringToDecimal]('123423sd.3333', 20,10))
select convert(decimal(20,10),[dbo].[StringToDecimal]('123423sd..3333', 20,10))
select convert(decimal(20,10),[dbo].[StringToDecimal]('-123423.3333', 20,10))
select convert(decimal(20,10),[dbo].[StringToDecimal]('+123423..3333', 20,10))
Thanks for the extra information. It sounds like you have three steps:
Remove all characters from the string that are not digits or a decimal point (do you ever get multiple points in one string?)
Convert to (9,5) or (4,1) as appropriate (how do you decide this? is there rounding? does 10X.781 become 10.78100 or 10.7 or 10.8?)
Insert/update the final value somewhere
Based on point 1 alone, I would immediately avoid TSQL and think about an external script or CLR procedure. A CLR function could do the parsing, but you still have the problem of returning different data types.
Since this appears to be some kind of ETL task, in my environment I would probably implement it as a script component in an SSIS package. The component would do the parsing and send the clean data to different outputs for further processing. If it was a one-time task I would use a Python script to parse the input data and generate INSERT or UPDATE statements.
I don't know if any of those solutions are suitable for you, but maybe it'll give you some ideas. And you should probably avoid the ISNUMERIC() function; search this site or Google to find some of the 'strange' input that it considers to be numeric.
Is there a straightforward way of finding the index of the last occurrence of a string using SQL? I am using SQL Server 2000 right now. I basically need the functionality that the .NET System.String.LastIndexOf method provides. A little googling revealed this - Function To Retrieve Last Index - but that does not work if you pass in a "text" column expression. Other solutions found elsewhere work only so long as the text you are searching for is 1 character long.
I will probably have to cook a function up. If I do so, I will post it here so you folks can look at it and maybe make use of.
Straightforward way? No, but I've used the reverse. Literally.
In prior routines, to find the last occurence of a given string, I used the REVERSE() function, followed CHARINDEX, followed again by REVERSE to restore the original order. For instance:
SELECT
mf.name
,mf.physical_name
,reverse(left(reverse(physical_name), charindex('\', reverse(physical_name)) -1))
from sys.master_files mf
shows how to extract the actual database file names from from their "physical names", no matter how deeply nested in subfolders. This does search for only one character (the backslash), but you can build on this for longer search strings.
The only downside is, I don't know how well this will work on TEXT data types. I've been on SQL 2005 for a few years now, and am no longer conversant with working with TEXT -- but I seem to recall you could use LEFT and RIGHT on it?
Philip
The simplest way is....
REVERSE(SUBSTRING(REVERSE([field]),0,CHARINDEX('[expr]',REVERSE([field]))))
If you are using Sqlserver 2005 or above, using REVERSE function many times is detrimental to performance, below code is more efficient.
DECLARE #FilePath VARCHAR(50) = 'My\Super\Long\String\With\Long\Words'
DECLARE #FindChar VARCHAR(1) = '\'
-- text before last slash
SELECT LEFT(#FilePath, LEN(#FilePath) - CHARINDEX(#FindChar,REVERSE(#FilePath))) AS Before
-- text after last slash
SELECT RIGHT(#FilePath, CHARINDEX(#FindChar,REVERSE(#FilePath))-1) AS After
-- the position of the last slash
SELECT LEN(#FilePath) - CHARINDEX(#FindChar,REVERSE(#FilePath)) + 1 AS LastOccuredAt
You are limited to small list of functions for text data type.
All I can suggest is start with PATINDEX, but work backwards from DATALENGTH-1, DATALENGTH-2, DATALENGTH-3 etc until you get a result or end up at zero (DATALENGTH-DATALENGTH)
This really is something that SQL Server 2000 simply can't handle.
Edit for other answers : REVERSE is not on the list of functions that can be used with text data in SQL Server 2000
DECLARE #FilePath VARCHAR(50) = 'My\Super\Long\String\With\Long\Words'
DECLARE #FindChar VARCHAR(1) = '\'
SELECT LEN(#FilePath) - CHARINDEX(#FindChar,REVERSE(#FilePath)) AS LastOccuredAt
Old but still valid question, so heres what I created based on the info provided by others here.
create function fnLastIndexOf(#text varChar(max),#char varchar(1))
returns int
as
begin
return len(#text) - charindex(#char, reverse(#text)) -1
end
REVERSE(SUBSTRING(REVERSE(ap_description),CHARINDEX('.',REVERSE(ap_description)),len(ap_description)))
worked better for me
This worked very well for me.
REVERSE(SUBSTRING(REVERSE([field]), CHARINDEX(REVERSE('[expr]'), REVERSE([field])) + DATALENGTH('[expr]'), DATALENGTH([field])))
Hmm, I know this is an old thread, but a tally table could do this in SQL2000 (or any other database):
DECLARE #str CHAR(21),
#delim CHAR(1)
SELECT #str = 'Your-delimited-string',
#delim = '-'
SELECT
MAX(n) As 'position'
FROM
dbo._Tally
WHERE
substring(#str, _Tally.n, 1) = #delim
A tally table is just a table of incrementing numbers.
The substring(#str, _Tally.n, 1) = #delim gets the position of each delimiter, then you just get the maximum position in that set.
Tally tables are awesome. If you haven't used them before, there is a good article on SQL Server Central.
*EDIT: Removed n <= LEN(TEXT_FIELD), as you can't use LEN() on the TEXT type. As long as the substring(...) = #delim remains though the result is still correct.
This answer uses MS SQL Server 2008 (I don't have access to MS SQL Server 2000), but the way I see it according to the OP are 3 situations to take into consideration. From what I've tried no answer here covers all 3 of them:
Return the last index of a search character in a given string.
Return the last index of a search sub-string (more than just a single
character) in a given string.
If the search character or sub-string is not in the given string return 0
The function I came up with takes 2 parameters:
#String NVARCHAR(MAX) : The string to be searched
#FindString NVARCHAR(MAX) : Either a single character or a sub-string to get the last
index of in #String
It returns an INT that is either the positive index of #FindString in #String or 0 meaning that #FindString is not in #String
Here's an explanation of what the function does:
Initializes #ReturnVal to 0 indicating that #FindString is not in #String
Checks the index of the #FindString in #String by using CHARINDEX()
If the index of #FindString in #String is 0, #ReturnVal is left as 0
If the index of #FindString in #String is > 0, #FindString is in #String so
it calculates the last index of #FindString in #String by using REVERSE()
Returns #ReturnVal which is either a positive number that is the last index of
#FindString in #String or 0 indicating that #FindString is not in #String
Here's the create function script (copy and paste ready):
CREATE FUNCTION [dbo].[fn_LastIndexOf]
(#String NVARCHAR(MAX)
, #FindString NVARCHAR(MAX))
RETURNS INT
AS
BEGIN
DECLARE #ReturnVal INT = 0
IF CHARINDEX(#FindString,#String) > 0
SET #ReturnVal = (SELECT LEN(#String) -
(CHARINDEX(REVERSE(#FindString),REVERSE(#String)) +
LEN(#FindString)) + 2)
RETURN #ReturnVal
END
Here's a little bit that conveniently tests the function:
DECLARE #TestString NVARCHAR(MAX) = 'My_sub2_Super_sub_Long_sub1_String_sub_With_sub_Long_sub_Words_sub2_'
, #TestFindString NVARCHAR(MAX) = 'sub'
SELECT dbo.fn_LastIndexOf(#TestString,#TestFindString)
I have only run this on MS SQL Server 2008 because I don't have access to any other version but from what I've looked into this should be good for 2008+ at least.
Enjoy.
Reverse both your string and your substring, then search for the first occurrence.
If you want to get the index of the last space in a string of words, you can use this expression
RIGHT(name, (CHARINDEX(' ',REVERSE(name),0)) to return the last word in the string. This is helpful if you want to parse out the last name of a full name that includes initials for the first and /or middle name.
Some of the other answers return an actual string whereas I had more need to know the actual index int. And the answers that do that seem to over-complicate things. Using some of the other answers as inspiration, I did the following...
First, I created a function:
CREATE FUNCTION [dbo].[LastIndexOf] (#stringToFind varchar(max), #stringToSearch varchar(max))
RETURNS INT
AS
BEGIN
RETURN (LEN(#stringToSearch) - CHARINDEX(#stringToFind,REVERSE(#stringToSearch))) + 1
END
GO
Then, in your query you can simply do this:
declare #stringToSearch varchar(max) = 'SomeText: SomeMoreText: SomeLastText'
select dbo.LastIndexOf(':', #stringToSearch)
The above should return 23 (the last index of ':')
Hope this made it a little easier for someone!
I realize this is a several years old question, but...
On Access 2010, you can use InStrRev() to do this. Hope this helps.
I know that it will be inefficient but have you considered casting the text field to varchar so that you can use the solution provided by the website you found? I know that this solution would create issues as you could potentially truncate the record if the length in the text field overflowed the length of your varchar (not to mention it would not be very performant).
Since your data is inside a text field (and you are using SQL Server 2000) your options are limited.
#indexOf = <whatever characters you are searching for in your string>
#LastIndexOf = LEN([MyField]) - CHARINDEX(#indexOf, REVERSE([MyField]))
Haven't tested, it might be off by one because of zero index, but works in SUBSTRING function when chopping off from #indexOf characters to end of your string
SUBSTRING([MyField], 0, #LastIndexOf)
This code works even if the substring contains more than 1 character.
DECLARE #FilePath VARCHAR(100) = 'My_sub_Super_sub_Long_sub_String_sub_With_sub_Long_sub_Words'
DECLARE #FindSubstring VARCHAR(5) = '_sub_'
-- Shows text before last substing
SELECT LEFT(#FilePath, LEN(#FilePath) - CHARINDEX(REVERSE(#FindSubstring), REVERSE(#FilePath)) - LEN(#FindSubstring) + 1) AS Before
-- Shows text after last substing
SELECT RIGHT(#FilePath, CHARINDEX(REVERSE(#FindSubstring), REVERSE(#FilePath)) -1) AS After
-- Shows the position of the last substing
SELECT LEN(#FilePath) - CHARINDEX(REVERSE(#FindSubstring), REVERSE(#FilePath)) AS LastOccuredAt
I needed to find the nth last position of a backslash in a folder path.
Here is my solution.
/*
http://stackoverflow.com/questions/1024978/find-index-of-last-occurrence-of-a-sub-string-using-t-sql/30904809#30904809
DROP FUNCTION dbo.GetLastIndexOf
*/
CREATE FUNCTION dbo.GetLastIndexOf
(
#expressionToFind VARCHAR(MAX)
,#expressionToSearch VARCHAR(8000)
,#Occurrence INT = 1 -- Find the nth last
)
RETURNS INT
AS
BEGIN
SELECT #expressionToSearch = REVERSE(#expressionToSearch)
DECLARE #LastIndexOf INT = 0
,#IndexOfPartial INT = -1
,#OriginalLength INT = LEN(#expressionToSearch)
,#Iteration INT = 0
WHILE (1 = 1) -- Poor man's do-while
BEGIN
SELECT #IndexOfPartial = CHARINDEX(#expressionToFind, #expressionToSearch)
IF (#IndexOfPartial = 0)
BEGIN
IF (#Iteration = 0) -- Need to compensate for dropping out early
BEGIN
SELECT #LastIndexOf = #OriginalLength + 1
END
BREAK;
END
IF (#Occurrence > 0)
BEGIN
SELECT #expressionToSearch = SUBSTRING(#expressionToSearch, #IndexOfPartial + 1, LEN(#expressionToSearch) - #IndexOfPartial - 1)
END
SELECT #LastIndexOf = #LastIndexOf + #IndexOfPartial
,#Occurrence = #Occurrence - 1
,#Iteration = #Iteration + 1
IF (#Occurrence = 0) BREAK;
END
SELECT #LastIndexOf = #OriginalLength - #LastIndexOf + 1 -- Invert due to reverse
RETURN #LastIndexOf
END
GO
GRANT EXECUTE ON GetLastIndexOf TO public
GO
Here are my test cases which pass
SELECT dbo.GetLastIndexOf('f','123456789\123456789\', 1) as indexOf -- expect 0 (no instances)
SELECT dbo.GetLastIndexOf('\','123456789\123456789\', 1) as indexOf -- expect 20
SELECT dbo.GetLastIndexOf('\','123456789\123456789\', 2) as indexOf -- expect 10
SELECT dbo.GetLastIndexOf('\','1234\6789\123456789\', 3) as indexOf -- expect 5
To get the part before the last occurence of the delimiter (works only for NVARCHAR due to DATALENGTH usage):
DECLARE #Fullstring NVARCHAR(30) = '12.345.67890.ABC';
DECLARE #Delimiter CHAR(1) = '.';
SELECT SUBSTRING(#Fullstring, 1, DATALENGTH(#Fullstring)/2 - CHARINDEX(#Delimiter, REVERSE(#Fullstring)));
This answer meets the requirements of the OP. specifically it allows the needle to be more than a single character and it does not generate an error when needle is not found in haystack. It seemed to me that most (all?) of the other answers did not handle those edge cases. Beyond that I added the "Starting Position" argument provided by the native MS SQL server CharIndex function. I tried to exactly mirror the specification for CharIndex except to process right to left instead of left to right. eg I return null if either needle or haystack is null and I return zero if needle is not found in haystack. One thing that I could not get around is that with the built in function the third parameter is optional. With SQL Server user defined functions, all parameters must be provided in the call unless the function is called using "EXEC" . While the third parameter must be included in the parameter list, you can provide the keyword "default" as a placeholder for it without having to give it a value (see examples below). Since it is easier to remove the third parameter from this function if not desired than it would be to add it if needed I have included it here as a starting point.
create function dbo.lastCharIndex(
#needle as varchar(max),
#haystack as varchar(max),
#offset as bigint=1
) returns bigint as begin
declare #position as bigint
if #needle is null or #haystack is null return null
set #position=charindex(reverse(#needle),reverse(#haystack),#offset)
if #position=0 return 0
return (len(#haystack)-(#position+len(#needle)-1))+1
end
go
select dbo.lastCharIndex('xyz','SQL SERVER 2000 USES ANSI SQL',default) -- returns 0
select dbo.lastCharIndex('SQL','SQL SERVER 2000 USES ANSI SQL',default) -- returns 27
select dbo.lastCharIndex('SQL','SQL SERVER 2000 USES ANSI SQL',1) -- returns 27
select dbo.lastCharIndex('SQL','SQL SERVER 2000 USES ANSI SQL',11) -- returns 1
I came across this thread while searching for a solution to my similar problem which had the exact same requirement but was for a different kind of database that was also lacking the REVERSE function.
In my case this was for a OpenEdge (Progress) database, which has a slightly different syntax. This made the INSTR function available to me that most Oracle typed databases offer.
So I came up with the following code:
SELECT
INSTR(foo.filepath, '/',1, LENGTH(foo.filepath) - LENGTH( REPLACE( foo.filepath, '/', ''))) AS IndexOfLastSlash
FROM foo
However, for my specific situation (being the OpenEdge (Progress) database) this did not result into the desired behaviour because replacing the character with an empty char gave the same length as the original string. This doesn't make much sense to me but I was able to bypass the problem with the code below:
SELECT
INSTR(foo.filepath, '/',1, LENGTH( REPLACE( foo.filepath, '/', 'XX')) - LENGTH(foo.filepath)) AS IndexOfLastSlash
FROM foo
Now I understand that this code won't solve the problem for T-SQL because there is no alternative to the INSTR function that offers the Occurence property.
Just to be thorough I'll add the code needed to create this scalar function so it can be used the same way like I did in the above examples.
-- Drop the function if it already exists
IF OBJECT_ID('INSTR', 'FN') IS NOT NULL
DROP FUNCTION INSTR
GO
-- User-defined function to implement Oracle INSTR in SQL Server
CREATE FUNCTION INSTR (#str VARCHAR(8000), #substr VARCHAR(255), #start INT, #occurrence INT)
RETURNS INT
AS
BEGIN
DECLARE #found INT = #occurrence,
#pos INT = #start;
WHILE 1=1
BEGIN
-- Find the next occurrence
SET #pos = CHARINDEX(#substr, #str, #pos);
-- Nothing found
IF #pos IS NULL OR #pos = 0
RETURN #pos;
-- The required occurrence found
IF #found = 1
BREAK;
-- Prepare to find another one occurrence
SET #found = #found - 1;
SET #pos = #pos + 1;
END
RETURN #pos;
END
GO
To avoid the obvious, when the REVERSE function is available you do not need to create this scalar function and you can just get the required result like this:
SELECT
LEN(foo.filepath) - CHARINDEX('/', REVERSE(foo.filepath))+1 AS LastIndexOfSlash
FROM foo
handles lookinng for something > 1 char long.
feel free to increase the parm sizes if you like.
couldnt resist posting
drop function if exists lastIndexOf
go
create function lastIndexOf(#searchFor varchar(100),#searchIn varchar(500))
returns int
as
begin
if LEN(#searchfor) > LEN(#searchin) return 0
declare #r varchar(500), #rsp varchar(100)
select #r = REVERSE(#searchin)
select #rsp = REVERSE(#searchfor)
return len(#searchin) - charindex(#rsp, #r) - len(#searchfor)+1
end
and tests
select dbo.lastIndexof('greg','greg greg asdflk; greg sadf' ) -- 18
select dbo.lastIndexof('greg','greg greg asdflk; grewg sadf' ) --5
select dbo.lastIndexof(' ','greg greg asdflk; grewg sadf' ) --24
This thread has been going for a while. I'll offer a solution covering different basis with example:
declare #aStringData varchar(100) = 'The quick brown/fox jumps/over the/lazy dog.pdf'
/*
The quick brown/fox jumps/over the/lazy dog.pdf
fdp.god yzal/eht revo/spmuj xof/nworb kciuq ehT
*/
select
Len(#aStringData) - CharIndex('/', Reverse(#aStringData)) + 1 [Char Index],
-- Get left side of character, without the character '/'
Left(#aStringData, Len(#aStringData) - CharIndex('/', Reverse(#aStringData))) [Left excluding char],
-- Get left side of character, including the character '/'
Left(#aStringData, Len(#aStringData) - CharIndex('/', Reverse(#aStringData)) + 1) [Left including char],
-- Get right side of character, without the character '/'
Right(#aStringData, CharIndex('/', Reverse(#aStringData)) - 1) [Right including char]
To get char position, need to reverse the string as CharIndex gets the first occurrence. Remembering as we're reversing, the CharIndex cursor will land on the other side of the character we're finding. So expect to compensate by -1 or +1, depending if wanting to get left or right side portion of string.
How can I know if a VARCHAR field's value can be successfully converted to an integer?
I want to do it massively to insert records from one table to another...
IsNumeric() function returns 1 for strings (varchars) which can be converted to a number and 0 for those that cannot..
Check out IsNumeric function
One issue whit IsNumeric() function is that You will get True and if number got decimal separator,
What is totally right, But if someone as I need to check straight to numbers in varchar, without decimal symbols, (I got that when I needed to calculate CHECK digit on barcode) You can use castom
made function like
create FUNCTION [dbo].[checkbarkod]
(
#ean_kod varchar(13)
)
RETURNS bit
AS
begin
declare #duzina int
declare #slovo char(1)
declare #pozicija int
declare #uredu bit
set #duzina=len(#ean_kod)
while #duzina>0
begin
set #slovo=(substring(#ean_kod,#duzina,1))
if (#slovo not in('1','2','3','4','5','6','7','8','9','0'))
begin
set #uredu=convert(bit,0)
break
end
else
begin
set #uredu=convert(bit,1)
set #duzina=#duzina-1
end
end
RETURN #uredu
end