T-SQL function for generating slugs? - tsql

Quick check to see if anyone has or knows of a T-SQL function capable of generating slugs from a given nvarchar input. i.e;
"Hello World" > "hello-world"
"This is a test" > "this-is-a-test"
I have a C# function that I normally use for these purposes, but in this case I have a large amount of data to parse and turn into slugs, so it makes more sense to do it on the SQL Server rather than have to transfer data over the wire.
As an aside, I don't have Remote Desktop access to the box so I can't run code (.net, Powershell etc) against it
Thanks in advance.
EDIT:
As per request, here's the function I generally use to generate slugs:
public static string GenerateSlug(string n, int maxLength)
{
string s = n.ToLower();
s = Regex.Replace(s, #"[^a-z0-9s-]", "");
s = Regex.Replace(s, #"[s-]+", " ").Trim();
s = s.Substring(0, s.Length <= maxLength ? s.Length : maxLength).Trim();
s = Regex.Replace(s, #"s", "-");
return s;
}

You can use LOWER and REPLACE to do this:
SELECT REPLACE(LOWER(origString), ' ', '-')
FROM myTable
For wholesale update of the column (the code sets the slug column according to the value of the origString column:
UPDATE myTable
SET slug = REPLACE(LOWER(origString), ' ', '-')

This is what I've come up with as a solution. Feel free to fix / modify where needed.
I should mention that the database I'm currently developing against is case insensitive hence the LOWER(#str).
CREATE FUNCTION [dbo].[UDF_GenerateSlug]
(
#str VARCHAR(100)
)
RETURNS VARCHAR(100)
AS
BEGIN
DECLARE #IncorrectCharLoc SMALLINT
SET #str = LOWER(#str)
SET #IncorrectCharLoc = PATINDEX('%[^0-9a-z ]%',#str)
WHILE #IncorrectCharLoc > 0
BEGIN
SET #str = STUFF(#str,#incorrectCharLoc,1,'')
SET #IncorrectCharLoc = PATINDEX('%[^0-9a-z ]%',#str)
END
SET #str = REPLACE(#str,' ','-')
RETURN #str
END
Mention to: http://blog.sqlauthority.com/2007/05/13/sql-server-udf-function-to-parse-alphanumeric-characters-from-string/ for the original code.

I know this is an old thread, but for future generation, I found one function that deals even with accents here:
CREATE function [dbo].[slugify](#string varchar(4000))
RETURNS varchar(4000) AS BEGIN
declare #out varchar(4000)
--convert to ASCII
set #out = lower(#string COLLATE SQL_Latin1_General_CP1251_CS_AS)
declare #pi int
--I'm sorry T-SQL have no regex. Thanks for patindex, MS .. :-)
set #pi = patindex('%[^a-z0-9 -]%',#out)
while #pi>0 begin
set #out = replace(#out, substring(#out,#pi,1), '')
--set #out = left(#out,#pi-1) + substring(#out,#pi+1,8000)
set #pi = patindex('%[^a-z0-9 -]%',#out)
end
set #out = ltrim(rtrim(#out))
-- replace space to hyphen
set #out = replace(#out, ' ', '-')
-- remove double hyphen
while CHARINDEX('--', #out) > 0 set #out = replace(#out, '--', '-')
return (#out)
END

Here's a variation of Jeremy's response. This might not technically be slugifying since I'm doing a couple of custom things like replacing "." with "-dot-", and stripping out apostrophes. Main improvement is this one also strips out all consecutive spaces, and doesn't strip out preexisting dashes.
create function dbo.Slugify(#str nvarchar(max)) returns nvarchar(max)
as
begin
declare #IncorrectCharLoc int
set #str = replace(replace(lower(#str),'.',' dot '),'''','')
-- remove non alphanumerics:
set #IncorrectCharLoc = patindex('%[^0-9a-z -]%',#str)
while #IncorrectCharLoc > 0
begin
set #str = stuff(#str,#incorrectCharLoc,1,' ')
set #IncorrectCharLoc = patindex('%[^0-9a-z -]%',#str)
end
-- remove consecutive spaces:
while charindex(' ',#str) > 0
begin
set #str = replace(#str, ' ', ' ')
end
set #str = replace(#str,' ','-')
return #str
end

I took Jeremy's response a couple steps further by removing all consecutive dashes even after spaces are replaced, and removed leading and trailing dashes.
create function dbo.Slugify(#str nvarchar(max)) returns nvarchar(max) as
begin
declare #IncorrectCharLoc int
set #str = replace(replace(lower(#str),'.','-'),'''','')
-- remove non alphanumerics:
set #IncorrectCharLoc = patindex('%[^0-9a-z -]%',#str)
while #IncorrectCharLoc > 0
begin
set #str = stuff(#str,#incorrectCharLoc,1,' ')
set #IncorrectCharLoc = patindex('%[^0-9a-z -]%',#str)
end
-- replace all spaces with dashes
set #str = replace(#str,' ','-')
-- remove consecutive dashes:
while charindex('--',#str) > 0
begin
set #str = replace(#str, '--', '-')
end
-- remove leading dashes
while charindex('-', #str) = 1
begin
set #str = RIGHT(#str, len(#str) - 1)
end
-- remove trailing dashes
while len(#str) > 0 AND substring(#str, len(#str), 1) = '-'
begin
set #str = LEFT(#str, len(#str) - 1)
end
return #str
end

-- Converts a title such as "This is a Test" to an all lower case string such
-- as "this-is-a-test" for use as the slug in a URL. All runs of separators
-- (whitespace, underscore, or hyphen) are converted to a single hyphen.
-- This is implemented as a state machine having the following four states:
--
-- 0 - initial state
-- 1 - in a sequence consisting of valid characters (a-z, A-Z, or 0-9)
-- 2 - in a sequence of separators (whitespace, underscore, or hyphen)
-- 3 - encountered a character that is neither valid nor a separator
--
-- Once the next state has been determined, the return value string is
-- built based on the transitions from the current state to the next state.
--
-- State 0 skips any initial whitespace. State 1 includes all valid slug
-- characters. State 2 converts multiple separators into a single hyphen
-- and skips trailing whitespace. State 3 skips any punctuation between
-- between characters and, if no additional whitespace is encountered,
-- then the punctuation is not treated as a word separator.
--
CREATE FUNCTION ToSlug(#title AS NVARCHAR(MAX))
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #retval AS VARCHAR(MAX) = ''; -- return value
DECLARE #i AS INT = 1; -- title index
DECLARE #c AS CHAR(1); -- current character
DECLARE #state AS INT = 0; -- current state
DECLARE #nextState AS INT; -- next state
DECLARE #tab AS CHAR(1) = CHAR(9); -- tab
DECLARE #lf AS CHAR(1) = CHAR(10); -- line feed
DECLARE #cr AS CHAR(1) = CHAR(13); -- carriage return
DECLARE #separators AS CHAR(8) = '[' + #tab + #lf + #cr + ' _-]';
DECLARE #validchars AS CHAR(11) = '[a-zA-Z0-9]';
WHILE (#i <= LEN(#title))
BEGIN
SELECT #c = SUBSTRING(#title, #i, 1),
#nextState = CASE
WHEN #c LIKE #validchars THEN 1
WHEN #state = 0 THEN 0
WHEN #state = 1 THEN CASE
WHEN #c LIKE #separators THEN 2
ELSE 3 -- unknown character
END
WHEN #state = 2 THEN 2
WHEN #state = 3 THEN CASE
WHEN #c LIKE #separators THEN 2
ELSE 3 -- stay in state 3
END
END,
#retval = #retval + CASE
WHEN #nextState != 1 THEN ''
WHEN #state = 0 THEN LOWER(#c)
WHEN #state = 1 THEN LOWER(#c)
WHEN #state = 2 THEN '-' + LOWER(#c)
WHEN #state = 3 THEN LOWER(#c)
END,
#state = #nextState,
#i = #i + 1
END
RETURN #retval;
END

To slug with Vietnamese unicode
CREATE function [dbo].[toslug](#string nvarchar(4000))
RETURNS varchar(4000) AS BEGIN
declare #out nvarchar(4000)
declare #from nvarchar(255)
declare #to varchar(255)
--convert to ASCII dbo.slugify
set #string = lower(#string)
set #out = #string
set #from = N'ýỳỷỹỵáàảãạâấầẩẫậăắằẳẵặéèẻẽẹêếềểễệúùủũụưứừửữựíìỉĩịóòỏõọơớờởỡợôốồổỗộđ·/_,:;'
set #to = 'yyyyyaaaaaaaaaaaaaaaaaeeeeeeeeeeeuuuuuuuuuuuiiiiioooooooooooooooood------'
declare #pi int
set #pi = 1
--I'm sorry T-SQL have no regex. Thanks for patindex, MS .. :-)
while #pi<=len(#from) begin
set #out = replace(#out, substring(#from,#pi,1), substring(#to,#pi,1))
set #pi = #pi + 1
end
set #out = ltrim(rtrim(#out))
-- replace space to hyphen
set #out = replace(#out, ' ', '-')
-- remove double hyphen
while CHARINDEX('--', #out) > 0 set #out = replace(#out, '--', '-')
return (#out)
END

Related

TSQL Replace Doubled Characters

Let's say I have data:
heloo
cuube
triniity
How to write script that will replace those "doubled" characters with only one? So the result from the above data set would be:
helo
cube
trinity
Usually I post some script where I tried to achieve this, but this time I can't think of any.
This should work:
CREATE PROCEDURE remove_duplicate_characters(#string VARCHAR(100))
AS
DECLARE #result VARCHAR(100)
SET #result=''
SELECT #result=#result+MIN(SUBSTRING(#string ,number,1)) FROM
(
SELECT number FROM master..spt_values WHERE type='p' AND number BETWEEN 1 AND len(#string )) AS t GROUP BY SUBSTRING(#string,number,1) ORDER BY MIN(number)
)
SELECT #result
GO
You then call it like this:
EXEC remove_duplicate_characters 'heloo'
Source
This script does not depend on having access to master functions, and just relies on t-sql string functions.
declare #word varchar(100) = 'aaaacuuuuuubeeeee', #result varchar(100) = ''
declare #letter char, #idx int = 0, #lastletter char = ''
while(#idx <= len(#word))
begin
select #letter = substring(#word,#idx,1)
if (#letter != #lastletter)
begin
select #result = concat(#result,#letter)
end
select #lastletter = #letter,#idx = #idx + 1
end
select #result

T-SQL Syntax - word parser

I have a word list in a variable. The words are comma delimited. I am trying to save them to individual record in a database. I found another question which does this and works, but it saves every word. I tried to modify it so that I only save unique words, and count duplicate as I go. I think the logic below is correct, but my syntax is not working.
if #FoundWord = 0 SET #WordsUsed = #WordsUsed + '' + #Word + ''
** is not concatenating the next word onto the end of the #WordsUsed variable
if #FoundWord = 0 INSERT INTO mydata.dbo.words (ProjectNumber, WordCount, Word) VALUES( '5', '1', #Word )
** doesn't seem to be doing anything at all ... I'm getting no records written to the words table
The entire code follows:
declare #SplitOn nvarchar(5) = ','
BEGIN
DECLARE #split_on_len INT = LEN(#SplitOn)
DECLARE #start_at INT = 1
DECLARE #end_at INT
DECLARE #data_len INT
DECLARE #WordsUsed varchar(max)
DECLARE #FoundWord int
DECLARE #Word varchar(100)
Set #WordsUsed = '**'
WHILE 1=1
BEGIN
SET #end_at = CHARINDEX(#SplitOn,#txt1,#start_at)
SET #data_len = CASE #end_at WHEN 0 THEN LEN(#txt1) ELSE #end_at-#start_at END
set #Word = SUBSTRING(#txt1,#start_at,#data_len)
SET #FoundWord = CHARINDEX('*' & #Word & '*', #WordsUsed)
if #FoundWord = 0 SET #WordsUsed = #WordsUsed & '*' & #Word & '*'
if #FoundWord = 0 INSERT INTO mydata.dbo.words (ProjectNumber, WordCount, Word) VALUES( '5', '1', #Word )
if #FoundWord > 0 Update mydata.dbo.words set WordCount = WordCount + 1 where projectnumber = 5 and word = #word
IF #end_at = 0 BREAK
SET #start_at = #end_at + #split_on_len
END
RETURN
END;
Here's a function you can try. It splits an unlimited-sized string based on the delimiter of your choice. The output of the function is a table - so you can then select distinct from that to store your word list. You'd call it something like:
insert YourWordlistTable ( Word ) --> just making up table/column names here
select distinct Data
from dbo.StringSplit( #yourVar, ',' )
Here's the definition of the function:
create function dbo.StringSplit
(
#string nvarchar( max ),
#delimiter nvarchar( 255 )
)
returns #t table ( Id int, Data nvarchar( 4000 ) )
as begin
with Split( startPosition, endPosition )
as
(
select
cast( 0 as bigint ) as startPosition,
charindex( #delimiter, #string ) as endPosition
union all
select
endPosition + 1, charindex( #delimiter, #string, endPosition + 1 )
from
Split
where
endPosition > 0
)
insert #t
select
row_number() over ( order by ( select 1 ) ) as Id,
substring( #string, startPosition, coalesce( nullif( endPosition, 0 ), len( #string ) + 1 ) - startPosition ) collate Latin1_General_CS_AS as Data
from
Split
option( maxrecursion 0 );
return;
end
I originally posted, then deleted, then reposted this when I realized I'd given an inline function I use that never got called on strings longer than 100 words. I've since modified it to support indefinite recursion - although it can't be an inline function this way.
Inline functions are generally faster because SQL can incorporate the inline function's statement into the query plan of the statements that call the function. However, that does not appear to be an option available in an inline function.
why can't you use
select count(distinct value) from dbo.split(',',#txt1) where #txt1 is he string will give you distinct count of words
If you are looking for word wise count
select value,count(1) from dbo.split(',',#txt1) group by value
shoud do this

Add comma every nth character in value

my problem is pretty simple. I get a value from a sql select which looks like this:
ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG
and I need it like this:
AR,AM,AU,BE,BA,BR,BG,CN,DK,DE,EE,FO,FI,FR,GE,GR,IE,IS,IT,JP,YU,CA,KZ,KG
The length is different in each dataset.
I tried it with format(), stuff() and so on but nothing brought me the result I need.
Thanks in advance
With a little help of a numbers table and for xml path.
-- Sample table
declare #T table
(
Value nvarchar(100)
)
-- Sample data
insert into #T values
('ARAMAU'),
('ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG')
declare #Len int
set #Len = 2;
select stuff(T2.X.value('.', 'nvarchar(max)'), 1, 1, '')
from #T as T1
cross apply (select ','+substring(T1.Value, 1+Number*#Len, #Len)
from Numbers
where Number >= 0 and
Number < len(T1.Value) / #Len
order by Number
for xml path(''), type) as T2(X)
Try on SE-Data
Time to update your resume.
create function DontDoThis (
#string varchar(max),
#count int
)
returns varchar(max)
as
begin
declare #result varchar(max) = ''
declare #token varchar(max) = ''
while DATALENGTH(#string) > 0
begin
select #token = left(#string, #count)
select #string = REPLACE(#string, #token, '')
select #result += #token + case when DATALENGTH(#string) = 0 then '' else ',' end
end
return #result
end
Call:
declare #test varchar(max) = 'ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG'
select dbo.DontDoThis(#test, 2)
gbn's comment is exactly right, if not very diplomatic :) TSQL is a poor language for string manipulation, but if you write a CLR function to do this then you will have the best of both worlds: .NET string functions called from pure TSQL.
I believe this is what QQping is looking for.
-- select .dbo.DelineateEachNth('ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG',2,',')
create function DelineateEachNth
(
#str varchar(max), -- Incoming String to parse
#length int, -- Length of desired segment
#delimiter varchar(100) -- Segment delimiter (comma, tab, line-feed, etc)
)
returns varchar(max)
AS
begin
declare #resultString varchar(max) = ''
-- only set delimiter(s) when lenght of string is longer than desired segment
if LEN(#str) > #length
begin
-- continue as long as there is a remaining string to parse
while len(#str) > 0
begin
-- as long as know we still need to create a segment...
if LEN(#str) > #length
begin
-- build result string from leftmost segment length
set #resultString = #resultString + left(#str, #length) + #delimiter
-- continually shorten result string by current segment
set #str = right(#str, len(#str) - #length)
end
-- as soon as the remaining string is segment length or less,
-- just use the remainder and empty the string to close the loop
else
begin
set #resultString = #resultString + #str
set #str = ''
end
end
end
-- if string is less than segment length, just pass it through
else
begin
set #resultString = #str
end
return #resultString
end
With a little help from Regex
select Wow=
(select case when MatchIndex %2 = 0 and MatchIndex!=0 then ',' + match else match end
from dbo.RegExMatches('[^\n]','ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG',1)
for xml path(''))

T-SQL VARCHAR(MAX) Truncated

DECLARE #str VARCHAR (MAX);
SELECT #str = COALESCE(#str + CHAR(10), '') +
'EXECUTE CreateDeno ' + CAST(ID AS VARCHAR)
FROM GL_To_Batch_Details
WHERE TYPE = 'C' AND
Deno_ID IS NULL;
--PRINT #str;--SELECT #str;
**EXEC(#str);**
EDITED
Does EXECUTE statement truncate strings to 8,000 chars like PRINT? How can I execute a dynamic SQL statement having more than 8,000 chars?
Any suggestion would be warmly appreciated.
PRINT is limited to 8k in output.
There is also an 8k limit in SSMS results pane.
Go to
tools -> options -> query results
to see the options.
To verify the length of the actual data, check:
SELECT LEN(#str)
When concatenating strings and the result is of type VARCHAR(MAX) and is over 8000 characters, at least one parameter and/or element being used in the concatenation need to be of the VARCHAR(MAX) type otherwise truncation will occur in the resultant string and will not be executable in an EXEC statement.
Example:
DECLARE #sql AS VARCHAR(MAX);
/* DECLARE #someItem AS VARCHAR(100); -- WILL CAUSE TRUNCATION WHEN #sql HAS LEN > 8000 */
DECLARE #someItem AS VARCHAR(MAX); -- All string variables need to be VARCHAR(MAX) when concatenating to another VARCHAR(MAX)
SET #someItem = 'Just assume the resulting #sql variable goes over 8000 characters...';
SET #sql = 'SELECT Something FROM Somewhere WHERE SomeField = ''' + #someItem + '''';
EXEC (#sql);
--PRINT #sql;
More information on MSDN.
"If the result of the concatenation of strings exceeds the limit of
8,000 bytes, the result is truncated. However, if at least one of the
strings concatenated is a large value type, truncation does not
occur."
The default length of a varchar is 30 characters:
CAST (ID AS VARCHAR)
Is it possible that id is longer than 30 characters?
The PRINT command is certainly limited to 8000 chars, irrespective of the length of the output (or whether it is varchar(max)). To work around this you need to output the string in chunks of <8000 chars
Update: In answer to your edit, exec doesn't limit the string length. I've put together the following example to show this:
DECLARE #str VARCHAR (MAX);
;WITH CTE_Count AS
(
select counter = 1
union all
select counter = counter+1
from CTE_Count
Where counter < 2000
)
SELECT
#str=COALESCE(#str + CHAR (10) ,
'' ) + 'select value=' + CAST (counter AS VARCHAR)
from
CTE_Count
Option (MAXRECURSION 0)
PRINT len(#str);--SELECT #str;
exec (#str)
Running this prints the length as 34892 chars, and all 2000 execute statements do run (be warned, it may take a few mins!)
It happens when you concatenate literals if one is not a varchar(max) the result ill be "implicit casted" to varchar(8000).
To generate a literal varchar(max) all parts must be varchar(max).
Note: It happened to me doing updates on varchar(max) columns, never tested with the EXEC command.
Also as noted in previous answers the print command holds a limit but you can try selecting that variable instead of printing it. (also ther's a limit on that select length you can configure on MS-SMS)
I also wanted to see what I was sending to Exec, and was confused by the PRINT limit. Had to write a proc to print in chunks.
CREATE PROCEDURE [dbo].[KFX_PrintVarcharMax]
#strMax varchar(max)
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#index int = 0,
#start int = 1,
#blkSize int = 2000;
WHILE #Start < LEN(#strMax)
BEGIN
IF #start + #blkSize >= LEN(#strMax)
BEGIN
-- If remainder is less than blocksize print the remainder, and exit.
PRINT SUBSTRING(#strMax, #start, #blkSize)
BREAK;
END
-- Else find the next terminator (beyond the blksize)
SET #index = CHARINDEX(CHAR(10), #strMax, #start + #blkSize);
if #index >= #start
BEGIN
PRINT SubString(#strMax, #start, #index - #start + 1)
SET #start = #index + 1;
SET #blkSize = CASE WHEN #start + 2000 < LEN(#strMax) THEN 2000
ELSE LEN(#strMax) - #start + 1 END
END
ELSE
BEGIN
-- No char(10) found. Just print the rest.
PRINT SUBSTRING(#strMax, #start, LEN(#strMax))
BREAK;
END
END
END

T-SQL Space Before Capital Letters

The question is self explanatory. Could you please point out a way to put spaces between each capital letter of a string.
SELECT dbo.SpaceBeforeCap('ThisIsATestString')
would result in
This Is A Test String.
This will add spaces only if the previous and next character is lowercase. That way 'MyABCAnalysis' will be 'My ABC Analysis'.
I added a check for a previous space too. Since some of our strings are prefixed with 'GR_' and some also contain underscores, we can use the replace function as follows:
select dbo.GR_SpaceBeforeCap(replace('GR_ABCAnalysis_Test','_',' '))
Returns 'GR ABC Analysis Test'
CREATE FUNCTION GR_SpaceBeforeCap (
#str nvarchar(max)
)
returns nvarchar(max)
as
begin
declare
#i int, #j int
, #cp nchar, #c0 nchar, #c1 nchar
, #result nvarchar(max)
select
#i = 1
, #j = len(#str)
, #result = ''
while #i <= #j
begin
select
#cp = substring(#str,#i-1,1)
, #c0 = substring(#str,#i+0,1)
, #c1 = substring(#str,#i+1,1)
if #c0 = UPPER(#c0) collate Latin1_General_CS_AS
begin
-- Add space if Current is UPPER
-- and either Previous or Next is lower
-- and Previous or Current is not already a space
if #c0 = UPPER(#c0) collate Latin1_General_CS_AS
and (
#cp <> UPPER(#cp) collate Latin1_General_CS_AS
or #c1 <> UPPER(#c1) collate Latin1_General_CS_AS
)
and #cp <> ' '
and #c0 <> ' '
set #result = #result + ' '
end -- if #co
set #result = #result + #c0
set #i = #i + 1
end -- while
return #result
end
Assuming SQL Server 2005 or later, this modified from code taken here: http://www.kodyaz.com/articles/case-sensitive-sql-split-function.aspx
CREATE FUNCTION SpaceBeforeCap
(
#str nvarchar(max)
)
returns nvarchar(max)
as
begin
declare #i int, #j int
declare #returnval nvarchar(max)
set #returnval = ''
select #i = 1, #j = len(#str)
declare #w nvarchar(max)
while #i <= #j
begin
if substring(#str,#i,1) = UPPER(substring(#str,#i,1)) collate Latin1_General_CS_AS
begin
if #w is not null
set #returnval = #returnval + ' ' + #w
set #w = substring(#str,#i,1)
end
else
set #w = #w + substring(#str,#i,1)
set #i = #i + 1
end
if #w is not null
set #returnval = #returnval + ' ' + #w
return ltrim(#returnval)
end
This can then be called just as you have suggested above.
This function combines previous answers. Selectively choose to preserve adjacent CAPS:
CREATE FUNCTION SpaceBeforeCap (
#InputString NVARCHAR(MAX),
#PreserveAdjacentCaps BIT
)
RETURNS NVARCHAR(MAX)
AS
BEGIN
DECLARE
#i INT, #j INT,
#previous NCHAR, #current NCHAR, #next NCHAR,
#result NVARCHAR(MAX)
SELECT
#i = 1,
#j = LEN(#InputString),
#result = ''
WHILE #i <= #j
BEGIN
SELECT
#previous = SUBSTRING(#InputString,#i-1,1),
#current = SUBSTRING(#InputString,#i+0,1),
#next = SUBSTRING(#InputString,#i+1,1)
IF #current = UPPER(#current) COLLATE Latin1_General_CS_AS
BEGIN
-- Add space if Current is UPPER
-- and either Previous or Next is lower or user chose not to preserve adjacent caps
-- and Previous or Current is not already a space
IF #current = UPPER(#current) COLLATE Latin1_General_CS_AS
AND (
#previous <> UPPER(#previous) COLLATE Latin1_General_CS_AS
OR #next <> UPPER(#next) collate Latin1_General_CS_AS
OR #PreserveAdjacentCaps = 0
)
AND #previous <> ' '
AND #current <> ' '
SET #result = #result + ' '
END
SET #result = #result + #current
SET #i = #i + 1
END
RETURN #result
END
GO
SELECT dbo.SpaceBeforeCap('ThisIsASampleDBString', 1)
GO
SELECT dbo.SpaceBeforeCap('ThisIsASampleDBString', 0)
CLR and regular expressions or 26 replace statements a case sensitive collate clause and a trim.
Another strategy would be to check the ascii value of each character:
create function SpaceBeforeCap
(#str nvarchar(max))
returns nvarchar(max)
as
begin
declare #result nvarchar(max)= left(#str, 1),
#i int = 2
while #i <= len(#str)
begin
if ascii(substring(#str, #i, 1)) between 65 and 90
select #result += ' '
select #result += substring(#str, #i, 1)
select #i += 1
end
return #result
end
/***
SELECT dbo.SpaceBeforeCap('ThisIsATestString')
**/
To avoid loops altogether, use of a tally table can help here. If you are running on SQL 2022, then the generate_series function can remove even this dependency. This method will be significantly faster than iterating through a loop.
create function core.ufnAddSpaceBeforeCapital
(
#inputString nvarchar(max)
)
returns nvarchar(max)
as
begin
declare #outputString nvarchar(max)
select
#outputString = string_agg(iif(t.value = 1, upper(substring(#inputString,t.value,1)),iif(ascii(substring(#inputString,t.value,1)) between 65 and 90, ' ','') + substring(#inputString,t.value,1)),'')
from
generate_series(1,cast(len(#inputString) as int)) t
return #outputString
end
The scalar function is not inlineable, so I've provided an alternative inline table-valued function if that's what you need.
create function core.ufnAddSpaceBeforeCapitalITVF
(
#inputString nvarchar(max)
)
returns table
as
return
(
select
string_agg(iif(t.value = 1, upper(substring(#inputString,t.value,1)),iif(ascii(substring(#inputString,t.value,1)) between 65 and 90, ' ','') + substring(#inputString,t.value,1)),'') as outputString
from
generate_series(1,cast(len(#inputString) as int)) t
)
end
While I really like the char looping answers I was not thrilled with the performance. I have found this performs in a fraction of the time for my use case.
CREATE function SpaceBeforeCap
(#examine nvarchar(max))
returns nvarchar(max)
as
begin
DECLARE #index as INT
SET #index = PatIndex( '%[^ ][A-Z]%', #examine COLLATE Latin1_General_BIN)
WHILE #index > 0 BEGIN
SET #examine = SUBSTRING(#examine, 1, #index) + ' ' + SUBSTRING(#examine, #index + 1, LEN(#examine))
SET #index = PatIndex( '%[^ ][A-Z]%', #examine COLLATE Latin1_General_BIN)
END
RETURN LTRIM(#examine)
end
This makes use of the fact that
case sensitive pattern search only works in some collations. The character class [^ ] means anything except space, so as we add the missing spaces we match farther into the string until it is complete.