Capitalize words in a string - sql

I've used INITCAP to capitalize words in a string, but I've run into a small issue:
select initcap(q'[JOE'S CARD 'N' CANDY]') from dual;
It returns "Joe'S Card 'N' Candy", but I wonder if there is another way to capitalize the words so it will look like this "Joe's Card 'N' Candy" (notice the s is in lowercase)

In your place I would create a custom PL/SQL procedure of the kind:
create or replace function initcap_cust(p_input varchar2)
return varchar2
as
l_input varchar2(4000) := lower(p_input);
l_capitalize_first_letter boolean := true;
l_output varchar2(4000) := null;
l_curr_char char(1);
begin
-- here we iterate over the lowercased string characters
for i in 1..length(l_input) loop
l_curr_char := substr(l_input, i, 1);
-- if we find a space - OK, next alphabet letter should be capitalized
-- you can add here more delimiters, e.g.: l_curr_char in (' ', ',', etc)
if l_curr_char = ' ' then
l_capitalize_first_letter := true;
end if;
-- makes O'Sullivan look this way
if regexp_like(l_output, '(^| )O''$') then
l_capitalize_first_letter := true;
end if;
-- found the first letter after delimiter - OK, capitalize
if l_capitalize_first_letter and (l_curr_char between 'a' and 'z') then
l_curr_char := upper(l_curr_char);
l_capitalize_first_letter := false;
end if;
-- build the output string
l_output := l_output || l_curr_char;
end loop;
return l_output;
end;
It works in your case and similar ones. Also it can be customized depending on your needs without dealing with complex queries built using the only functions provided by Oracle out of the box.
N.B. Also there is an option to create equivalent java stored procedure, on the link provided by Edgars T. there is an example.

Adapted from this answer to use a single simpler regular expression to parse each word:
WITH names ( name ) AS (
SELECT 'FIRSTNAME O''MALLEY' FROM DUAL UNION
SELECT 'FIRST''NAME TEH''TE' FROM DUAL UNION
SELECT 'FORMAT ME BYGGER''N' FROM DUAL UNION
SELECT 'OLD MCDONALD' FROM DUAL UNION
SELECT 'EVEN OL''DER MACDONALD' FROM DUAL UNION
SELECT q'[JOE'S CARD 'N' CANDY]' FROM DUAL
)
SELECT name,
formatted_name
FROM names
MODEL
PARTITION BY (ROWNUM rn)
DIMENSION BY (0 dim)
MEASURES(name, CAST('' AS VARCHAR2(255)) word, CAST('' AS VARCHAR(255)) formatted_name)
RULES ITERATE(99) UNTIL (word[0] IS NULL)
(
word[0] = REGEXP_SUBSTR(name[0], '[^ ]+( *|$)', 1, ITERATION_NUMBER + 1),
formatted_name[0] = formatted_name[0]
-- Capitalise names starting with ', *', MC and MAC:
|| INITCAP(REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 1 ) )
-- Capitalise the next letter of the word
|| UPPER( REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 2 ) )
-- Lower case the rest of the word
|| LOWER( REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 3 ) )
);
Output:
NAME FORMATTED_NAME
----------------------- ----------------------
EVEN OL'DER MACDONALD Even Ol'der MacDonald
OLD MCDONALD Old McDonald
FIRST'NAME TEH'TE First'name Teh'te
FORMAT ME BYGGER'N Format Me Bygger'n
JOE'S CARD 'N' CANDY Joe's Card 'N' Candy
FIRSTNAME O'MALLEY Firstname O'Malley

Related

How to count the number of multiple repeating characters in a column and get a list of it in Oracle? [duplicate]

How can I count number of occurrences of the character - in a varchar2 string?
Example:
select XXX('123-345-566', '-') from dual;
----------------------------------------
2
Here you go:
select length('123-345-566') - length(replace('123-345-566','-',null))
from dual;
Technically, if the string you want to check contains only the character you want to count, the above query will return NULL; the following query will give the correct answer in all cases:
select coalesce(length('123-345-566') - length(replace('123-345-566','-',null)), length('123-345-566'), 0)
from dual;
The final 0 in coalesce catches the case where you're counting in an empty string (i.e. NULL, because length(NULL) = NULL in ORACLE).
REGEXP_COUNT should do the trick:
select REGEXP_COUNT('123-345-566', '-') from dual;
Here's an idea: try replacing everything that is not a dash char with empty string. Then count how many dashes remained.
select length(regexp_replace('123-345-566', '[^-]', '')) from dual
I justed faced very similar problem... BUT RegExp_Count couldn't resolved it.
How many times string '16,124,3,3,1,0,' contains ',3,'? As we see 2 times, but RegExp_Count returns just 1. Same thing is with ''bbaaaacc' and when looking in it 'aa' - should be 3 times and RegExp_Count returns just 2.
select REGEXP_COUNT('336,14,3,3,11,0,' , ',3,') from dual;
select REGEXP_COUNT('bbaaaacc' , 'aa') from dual;
I lost some time to research solution on web. Couldn't' find... so i wrote my own function that returns TRUE number of occurance. Hope it will be usefull.
CREATE OR REPLACE FUNCTION EXPRESSION_COUNT( pEXPRESSION VARCHAR2, pPHRASE VARCHAR2 ) RETURN NUMBER AS
vRET NUMBER := 0;
vPHRASE_LENGTH NUMBER := 0;
vCOUNTER NUMBER := 0;
vEXPRESSION VARCHAR2(4000);
vTEMP VARCHAR2(4000);
BEGIN
vEXPRESSION := pEXPRESSION;
vPHRASE_LENGTH := LENGTH( pPHRASE );
LOOP
vCOUNTER := vCOUNTER + 1;
vTEMP := SUBSTR( vEXPRESSION, 1, vPHRASE_LENGTH);
IF (vTEMP = pPHRASE) THEN
vRET := vRET + 1;
END IF;
vEXPRESSION := SUBSTR( vEXPRESSION, 2, LENGTH( vEXPRESSION ) - 1);
EXIT WHEN ( LENGTH( vEXPRESSION ) = 0 ) OR (vEXPRESSION IS NULL);
END LOOP;
RETURN vRET;
END;
I thought of
SELECT LENGTH('123-345-566') - LENGTH(REPLACE('123-345-566', '-', '')) FROM DUAL;
You can try this
select count( distinct pos) from
(select instr('123-456-789', '-', level) as pos from dual
connect by level <=length('123-456-789'))
where nvl(pos, 0) !=0
it counts "properly" olso for how many 'aa' in 'bbaaaacc'
select count( distinct pos) from
(select instr('bbaaaacc', 'aa', level) as pos from dual
connect by level <=length('bbaaaacc'))
where nvl(pos, 0) !=0
here is a solution that will function for both characters and substrings:
select (length('a') - nvl(length(replace('a','b')),0)) / length('b')
from dual
where a is the string in which you search the occurrence of b
have a nice day!
SELECT {FN LENGTH('123-345-566')} - {FN LENGTH({FN REPLACE('123-345-566', '#', '')})} FROM DUAL
select count(*)
from (
select substr('K_u_n_a_l',level,1) str
from dual
connect by level <=length('K_u_n_a_l')
)
where str ='_';

Oracle Regexp_ help to do a replace on string

I have a field that has several name values seperated by $;, like below:
$;James$;Paul$;
I have a function available on the names that allows me to identify data from a different table i.e. email address.
However the function only works on a single name value.
So I need to do a substring to get each name value and then run a replace command that runs the function to pull out their email address.
I want to get the following responses James#emailserver.com, Paul#emailserver.com.
So my question is: how can I run a regexp_replace command to identify the names and then split them out to run the replace?
Something like this:
with
name_list ( id, str ) as (
select 1, '$;James$;Paul$;' from dual union all
select 2, '$;Jane$;Emily$;Ann$;' from dual
)
select id, substr(str, instr(str, '$;', 1, level) + 2,
instr(str, '$;', 1, level+1) - instr(str, '$;', 1, level) - 2) as name
from name_list
connect by level <= regexp_count(str, '\$;') - 1
and prior id = id
and prior sys_guid() is not null
;
ID NAME
-- -----
1 James
1 Paul
2 Jane
2 Emily
2 Ann
And then you can use this for your comparisons.
Alternatively, if you need to see if the name James is in your input string (and assuming James is the value in a column name_col):
... where name_list.str like '$;' || name_col || '$;'
and you don't need to split the string anymore. The concatenations are necessary, though, because you don't want Anne in the name list to match the name Ann in the column.
This function helps me to split a varchar:
function f_split_string( p_string varchar2, p_separator varchar2, p_pos number) return varchar2
is
v_string varchar2(100);
begin
select partition into v_string from
(select rownum as n , partition from
(select regexp_substr(p_string, '[^'||p_separator||']+', 1, level) partition from dual
connect by regexp_substr(p_string, '[^'||p_separator||']+', 1, level) is not null)
) t1
where t1.n = p_pos;
return trim(v_string);
exception
when others then return 'nfound';
end;
Example call:
/*using the params*/
p_string := '$;James$;Paul$;'; --string to parse
p_separator := '$;'; --separator
p_pos := 1; --position to get
f_split_string(p_string,p_separator,p_pos) /*return 'James'*/
/*if p_pos :=2*/
f_split_string(p_string,p_separator,p_pos) /* return 'Paul' */
/*if p_pos :=3*/
f_split_string(p_string,p_separator,p_pos) /*return 'nfound' */
/*if p_pos :=0*/
f_split_string(p_string,p_separator,p_pos) /* return 'nfound' */

Comparing two comma delimited strings

I have 2 strings str1: 'abc,def,ghi' and str2: 'tyu,abc,fgh'.
I want to compare these two strings using the delimiter ,. Now since the 2 strings have abc it should return true. I want a function in Oracle SQL which can perform this operation.
Looks complicated but it is just a couple of helper functions to split a list into separate values (to be contained into a table type) and then a very simple function to test the intersection of two collections.
Oracle Setup:
CREATE TYPE VARCHAR2s_Table AS TABLE OF VARCHAR2(4000);
CREATE FUNCTION regexp_escape(
expression VARCHAR2
) RETURN VARCHAR2 DETERMINISTIC
AS
BEGIN
RETURN REGEXP_REPLACE( expression, '([$^[()+*?{\|])', '\\\1', 1, 0, 'c' );
END;
/
CREATE FUNCTION splitList(
list VARCHAR2,
delim VARCHAR2 := ','
) RETURN VARCHAR2s_Table DETERMINISTIC
AS
pattern VARCHAR2(256);
len BINARY_INTEGER;
t_items VARCHAR2s_Table := VARCHAR2s_Table();
BEGIN
IF list IS NULL THEN
NULL;
ELSIF delim IS NULL THEN
t_items.EXTEND( LENGTH( list ) );
FOR i IN 1 .. LENGTH( list ) LOOP
t_items(i) := SUBSTR( list, i, 1 );
END LOOP;
ELSE
pattern := '(.*?)($|' || REGEXP_ESCAPE( delim ) || ')';
len := REGEXP_COUNT( list, pattern ) - 1;
t_items.EXTEND( len );
IF len = 1 THEN
t_items(1) := list;
ELSE
FOR i IN 1 .. len LOOP
t_items(i) := REGEXP_SUBSTR( list, pattern, 1, i, NULL, 1 );
END LOOP;
END IF;
END IF;
RETURN t_items;
END;
/
CREATE FUNCTION check_list_intersect(
list1 VARCHAR2,
list2 VARCHAR2
) RETURN NUMBER DETERMINISTIC
AS
BEGIN
IF splitList( list1 ) MULTISET INTERSECT splitList( list2 ) IS EMPTY THEN
RETURN 0;
ELSE
RETURN 1;
END IF;
END;
/
Query 1:
SELECT check_list_intersect( 'abc,def,ghi', 'abc' ) AS matches
FROM DUAL;
Results:
MATCHES
---------
1
Query 2:
SELECT check_list_intersect( 'abc,def,ghi', 'abcd' ) AS matches
FROM DUAL;
Results:
MATCHES
---------
0
The below will make the trick.
with temp as (
select 1 strid, 'abc,def,ghi' Error from dual
union all
select 2, 'tyu,abc,fgh' from dual
)
select str
from (
SELECT strid, trim(regexp_substr(str, '[^,]+', 1, level)) str
FROM (SELECT strid, Error str FROM temp) t
CONNECT BY instr(str, ',', 1, level - 1) > 0
)
group by str
having count(distinct strid) > 1;
Warning: This answer is not fully correct. (See the comments.)
Starting from dcieslak answer(csv split with regexp), a variation of the subject would be:
create or replace function check_string_intersec(str1 varchar2, str2 varchar2) return number
is
begin
for k in (SELECT trim(regexp_substr(str1, '[^,]+', 1, level)) item
FROM dual
CONNECT BY instr(str1, ',', 1, level - 1) > 0
)
loop
if instr(str2, k.item,1) > 0 then return 1; end if;
end loop;
return 0;
end;
This splits first string and search for every item in the second string.

How to count the number of occurrences of a character in an Oracle varchar value?

How can I count number of occurrences of the character - in a varchar2 string?
Example:
select XXX('123-345-566', '-') from dual;
----------------------------------------
2
Here you go:
select length('123-345-566') - length(replace('123-345-566','-',null))
from dual;
Technically, if the string you want to check contains only the character you want to count, the above query will return NULL; the following query will give the correct answer in all cases:
select coalesce(length('123-345-566') - length(replace('123-345-566','-',null)), length('123-345-566'), 0)
from dual;
The final 0 in coalesce catches the case where you're counting in an empty string (i.e. NULL, because length(NULL) = NULL in ORACLE).
REGEXP_COUNT should do the trick:
select REGEXP_COUNT('123-345-566', '-') from dual;
Here's an idea: try replacing everything that is not a dash char with empty string. Then count how many dashes remained.
select length(regexp_replace('123-345-566', '[^-]', '')) from dual
I justed faced very similar problem... BUT RegExp_Count couldn't resolved it.
How many times string '16,124,3,3,1,0,' contains ',3,'? As we see 2 times, but RegExp_Count returns just 1. Same thing is with ''bbaaaacc' and when looking in it 'aa' - should be 3 times and RegExp_Count returns just 2.
select REGEXP_COUNT('336,14,3,3,11,0,' , ',3,') from dual;
select REGEXP_COUNT('bbaaaacc' , 'aa') from dual;
I lost some time to research solution on web. Couldn't' find... so i wrote my own function that returns TRUE number of occurance. Hope it will be usefull.
CREATE OR REPLACE FUNCTION EXPRESSION_COUNT( pEXPRESSION VARCHAR2, pPHRASE VARCHAR2 ) RETURN NUMBER AS
vRET NUMBER := 0;
vPHRASE_LENGTH NUMBER := 0;
vCOUNTER NUMBER := 0;
vEXPRESSION VARCHAR2(4000);
vTEMP VARCHAR2(4000);
BEGIN
vEXPRESSION := pEXPRESSION;
vPHRASE_LENGTH := LENGTH( pPHRASE );
LOOP
vCOUNTER := vCOUNTER + 1;
vTEMP := SUBSTR( vEXPRESSION, 1, vPHRASE_LENGTH);
IF (vTEMP = pPHRASE) THEN
vRET := vRET + 1;
END IF;
vEXPRESSION := SUBSTR( vEXPRESSION, 2, LENGTH( vEXPRESSION ) - 1);
EXIT WHEN ( LENGTH( vEXPRESSION ) = 0 ) OR (vEXPRESSION IS NULL);
END LOOP;
RETURN vRET;
END;
I thought of
SELECT LENGTH('123-345-566') - LENGTH(REPLACE('123-345-566', '-', '')) FROM DUAL;
You can try this
select count( distinct pos) from
(select instr('123-456-789', '-', level) as pos from dual
connect by level <=length('123-456-789'))
where nvl(pos, 0) !=0
it counts "properly" olso for how many 'aa' in 'bbaaaacc'
select count( distinct pos) from
(select instr('bbaaaacc', 'aa', level) as pos from dual
connect by level <=length('bbaaaacc'))
where nvl(pos, 0) !=0
here is a solution that will function for both characters and substrings:
select (length('a') - nvl(length(replace('a','b')),0)) / length('b')
from dual
where a is the string in which you search the occurrence of b
have a nice day!
SELECT {FN LENGTH('123-345-566')} - {FN LENGTH({FN REPLACE('123-345-566', '#', '')})} FROM DUAL
select count(*)
from (
select substr('K_u_n_a_l',level,1) str
from dual
connect by level <=length('K_u_n_a_l')
)
where str ='_';

SQL sort by version "number", a string of varying length

I'm trying to create an SQL query that will order the results by a version number (e.g. 1.1, 4.5.10, etc.)
Here's what I tried:
SELECT * FROM Requirements
WHERE Requirements.Release NOT LIKE '%Obsolete%'
ORDER BY Requirements.ReqNum
Now, the ReqNum field is a string field and unfortunately I can't change it to a float or something like that because I have requirement numbers like 162.1.11.
When I get the results back, I'll get ordering like this:
1.1
1.10
1.11
1.3
How can I write a query that will sort by lexicographic order?
... or,
How can I correctly sort the data?
Thanks for the input in advance!
In PostgreSQL you can do:
SELECT * FROM Requirements
ORDER BY string_to_array(version, '.')::int[];
This last ::int[] makes it convert string values into integers and then compare as such.
For best results, refactor version number storage so that each section has it's own column: MajorVersion, MinorVersion, Revision, Build. Then the ordering problem suddenly becomes trivial. You can also build a computed column for easy retrieval of the full string.
SELECT * FROM Requirements
WHERE Requirements.Release NOT LIKE '%Obsolete%'
ORDER BY cast('/' + replace(Requirements.ReqNum , '.', '/') + '/' as hierarchyid);
A slight variation on #vuttipong-l answer (T-SQL)
SELECT VersionNumber
FROM (
SELECT '6.1.3' VersionNumber UNION
SELECT '6.11.3' UNION
SELECT '6.2.3' UNION
SELECT '6.1.12'
) AS q
ORDER BY cast('/' + VersionNumber + '/' as hierarchyid)
Works in SQL Server starting with 2008, dots are OK in a string representation of a hierarchyid column, so we don't need to replace them with slashes.
A quote from the doc:
Comparison is performed by comparing the integer sequences separated
by dots in dictionary order.
There's one caveat though: the version segments must not be prefixed with zeroes.
If you are in SQL Server land...
DECLARE #string varchar(40)
SET #string = '1.2.3.4'
SELECT PARSENAME(#string, 1), PARSENAME(#string, 2), PARSENAME(#string, 3), PARSENAME(#string, 4)
Results:
4, 3, 2, 1
Useful for parsing IP Addresses and other dotted items, such as a version number. (You can use REPLACE() to convert items into dotted notation too... e.g. 1-2-3-4 -> 1.2.3.4)
If you don't re-design the table as Joel Coehoorn sensibly suggests, then you need to re-format the version numbers to a string that sorts as you require, e.g.
1.1 -> 0001.0001.0000
162.1.11 -> 0162.0001.0011
This could be done by a function, or using a computed/virtual column if your DBMS has these. Then you can use that function or column in the ORDER BY clause.
The following function will take a version number and format each level out to 3 digits:
Usage:
select * from TableX order by dbo.fn_VersionPad(VersionCol1)
Function:
CREATE FUNCTION [dbo].[fn_VersionPad]
(
#version varchar(20)
)
RETURNS varchar(20)
AS
BEGIN
/*
Purpose: Pads multi-level Version Number sections to 3 digits
Example: 1.2.3.4
Returns: 001.002.003.004
*/
declare #verPad varchar(20)
declare #i int
declare #digits int
set #verPad = ''
set #i = len(#version)
set #digits = 0
while #i > 0
begin
if (substring(#version, #i, 1) = '.')
begin
while (#digits < 3)
begin
-- Pad version level to 3 digits
set #verPad = '0' + #verPad
set #digits = #digits + 1
end
set #digits = -1
end
set #verPad = substring(#version, #i, 1) + #verPad
set #i = #i - 1
set #digits = #digits + 1
end
while (#digits < 3)
begin
-- Pad version level to 3 digits
set #verPad = '0' + #verPad
set #digits = #digits + 1
end
return #verPad
END
You could split up the string (you already know the delimiters: ".") with CHARINDEX / SUBSTR and ORDER BY the different parts. Do it in a function or do it part by part.
It won't be pretty and it won't be fast: so if you need fast queries, follow Tony or Joel.
NOT USİNG CODE
Insert into #table
Select 'A1' union all
Select 'A3' union all
Select 'A5' union all
Select 'A15' union all
Select 'A11' union all
Select 'A10' union all
Select 'A2' union all
Select 'B2' union all
Select 'C2' union all
Select 'C22' union all
Select 'C221' union all
Select 'A7'
Select cod from #table
Order by LEN(cod),cod
Result :
A1
A2
A3
A5
A7
B2
C2
A10
A11
A15
C22
C221
It's simple as:
Declare #table table(id_ int identity(1,1), cod varchar(10))
Insert into #table
Select 'A1' union all
Select 'A3' union all
Select 'A5' union all
Select 'A15' union all
Select 'A11' union all
Select 'A10' union all
Select 'A2' union all
Select 'A7'
Select cod from #table
Order by LEN(cod),cod
On PostgreSQL, it couldn't be easier:
SELECT ver_no FROM version ORDER BY string_to_array(ver_no, '.', '')::int[]
This would work if you're using Microsoft SQL Server:
create function fnGetVersion (#v AS varchar(50)) returns bigint as
begin
declare #n as bigint;
declare #i as int;
select #n = 0;
select #i = charindex('.',#v);
while(#i > 0)
begin
select #n = #n * 1000;
select #n = #n + cast(substring(#v,1,#i-1) as bigint);
select #v = substring(#v,#i+1,len(#v)-#i);
select #i = charindex('.',#v);
end
return #n * 1000 + cast(#v as bigint);
end
Test by running this command:
select dbo.fnGetVersion('1.2.3.4')
That would return the number 1002003004 wich is perfectly sortable. Is you need 9.0.1 to be bigger than 2.1.2.3 then you would need to change the logic slightly. In my example 9.0.1 would be sorted before 2.1.2.3.
Function for PostgreSQL
Simply use
select *
from sample_table
order by _sort_version(column_version);
CREATE FUNCTION _sort_version (
p_version text
)
RETURNS text AS
$body$
declare
v_tab text[];
begin
v_tab := string_to_array(p_version, '.');
for i in 1 .. array_length(v_tab, 1) loop
v_tab[i] := lpad(v_tab[i], 4, '0');
end loop;
return array_to_string(v_tab, '.');
end;
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY DEFINER
COST 1;
I've had the same problem, though mine was with apartment numbers like A1, A2, A3, A10, A11, etc, that they wanted to sort "right". If splitting up the version number into separate columns doesn't work, try this PL/SQL. It takes a string like A1 or A10and expands it into A0000001, A0000010, etc, so it sorts nicely. Just call this in ORDER BY clause, like
select apt_num
from apartment
order by PAD(apt_num)
function pad(inString IN VARCHAR2)
return VARCHAR2
--This function pads the numbers in a alphanumeric string.
--It is particularly useful in sorting, things like "A1, A2, A10"
--which would sort like "A1, A10, A2" in a standard "ORDER BY name" clause
--but by calling "ORDER BY pkg_sort.pad(name)" it will sort as "A1, A2, A10" because this
--function will convert it to "A00000000000000000001, A00000000000000000002, A00000000000000000010"
--(but since this is in the order by clause, it will
--not be displayed.
--currently, the charTemplate variable pads the number to 20 digits, so anything up to 99999999999999999999
--will work correctly.
--to increase the size, just change the charTemplate variable. If the number is larger than 20 digits, it will just
--appear without padding.
is
outString VARCHAR2(255);
numBeginIndex NUMBER;
numLength NUMBER;
stringLength NUMBER;
i NUMBER;
thisChar VARCHAR2(6);
charTemplate VARCHAR2(20) := '00000000000000000000';
charTemplateLength NUMBER := 20;
BEGIN
outString := null;
numBeginIndex := -1;
numLength := 0;
stringLength := length(inString);
--loop through each character, get that character
FOR i IN 1..(stringLength) LOOP
thisChar := substr(inString, i, 1);
--if this character is a number
IF (FcnIsNumber(thisChar)) THEN
--if we haven't started a number yet
IF (numBeginIndex = -1) THEN
numBeginIndex := i;
numLength := 1;
--else if we're in a number, increase the length
ELSE
numLength := numLength + 1;
END IF;
--if this is the last character, we have to append the number
IF (i = stringLength) THEN
outString:= FcnConcatNumber(inString, outString, numBeginIndex, numLength, charTemplate, charTemplateLength);
END IF;
--else this is a character
ELSE
--if we were previously in a number, concat that and reset the numBeginIndex
IF (numBeginIndex != -1) THEN
outString:= FcnConcatNumber(inString, outString, numBeginIndex, numLength, charTemplate, charTemplateLength);
numBeginIndex := -1;
numLength := 0;
END IF;
--concat the character
outString := outString || thisChar;
END IF;
END LOOP;
RETURN outString;
--any exception, just return the original string
EXCEPTION WHEN OTHERS THEN
RETURN inString;
END;
Here is an example query that extracts the string. You should be able to use this in either the UPDATE refactoring of the database, or simply in your query as-is. However, I'm not sure how it is on time; just something to watch out and test for.
SELECT SUBSTRING_INDEX("1.5.32",'.',1) AS MajorVersion,
SUBSTRING_INDEX(SUBSTRING_INDEX("1.5.32",'.',-2),'.',1) AS MinorVersion,
SUBSTRING_INDEX("1.5.32",'.',-1) AS Revision;
this will return:
MajorVersion | MinorVersion | Revision
1 | 5 | 32
Ok, if high performance is an issue then your only option is to change your values into something numeric.
However, if this is a low usage query then you can just split your numbers and order by those.
This query assumes just major and minor version numbers and that they contain just numbers.
SELECT
*
FROM
Requirements
WHERE
Requirements.Release NOT LIKE '%Obsolete%'
ORDER BY
CONVERT(int, RIGHT(REPLICATE('0', 10) + LEFT(Requirements.ReqNum, CHARINDEX('.', Requirements.ReqNum)-1), 10)),
CONVERT(int, SUBSTRING(Requirements.ReqNum, CHARINDEX('.', Requirements.ReqNum )+1, LEN(Requirements.ReqNum) - CHARINDEX('.', Requirements.ReqNum )))
For the all-in-one-query purists, assuming Oracle, some instr/substr/decode/to_number voodoo can solve it:
SELECT *
FROM Requirements
WHERE Release NOT LIKE '%Obsolete%'
ORDER BY
to_number(
substr( reqnum, 1, instr( reqnum, '.' ) - 1 )
)
, to_number(
substr(
reqnum
, instr( reqnum, '.' ) + 1 -- start: after first occurance
, decode(
instr( reqnum, '.', 1, 2 )
, 0, length( reqnum )
, instr( reqnum, '.', 1, 2 ) - 1
) -- second occurance (or end)
- instr( reqnum, '.', 1, 1) -- length: second occurance (or end) less first
)
)
, to_number(
decode(
instr( reqnum, '.', 1, 2 )
, 0, null
, substr(
reqnum
, instr( reqnum, '.', 1, 2 ) + 1 -- start: after second occurance
, decode(
instr( reqnum, '.', 1, 3 )
, 0, length( reqnum )
, instr( reqnum, '.', 1, 3 ) - 1
) -- third occurance (or end)
- instr( reqnum, '.', 1, 2) -- length: third occurance (or end) less second
)
)
)
, to_number(
decode(
instr( reqnum, '.', 1, 3 )
, 0, null
, substr(
reqnum
, instr( reqnum, '.', 1, 3 ) + 1 -- start: after second occurance
, decode(
instr( reqnum, '.', 1, 4 )
, 0, length( reqnum )
, instr( reqnum, '.', 1, 4 ) - 1
) -- fourth occurance (or end)
- instr( reqnum, '.', 1, 3) -- length: fourth occurance (or end) less third
)
)
)
;
I suspect there are plenty of caveats including:
assumption of the presence of minor version (second)
limited to four versions as specified in question's comments
Here's a comparison function for PostgreSQL that will compare arbitrary strings such that sequences of digits are compared numerically. In other words, "ABC123" > "ABC2", but "AB123" < "ABC2". It returns -1, 0, or +1 as such comparison functions usually do.
CREATE FUNCTION vercmp(a text, b text) RETURNS integer AS $$
DECLARE
ar text[];
br text[];
n integer := 1;
BEGIN
SELECT array_agg(y) INTO ar FROM (SELECT array_to_string(regexp_matches(a, E'\\d+|\\D+|^$', 'g'),'') y) x;
SELECT array_agg(y) INTO br FROM (SELECT array_to_string(regexp_matches(b, E'\\d+|\\D+|^$', 'g'),'') y) x;
WHILE n <= array_length(ar, 1) AND n <= array_length(br, 1) LOOP
IF ar[n] ~ E'^\\d+$' AND br[n] ~ E'^\\d+$' THEN
IF ar[n]::integer < br[n]::integer THEN
RETURN -1;
ELSIF ar[n]::integer > br[n]::integer THEN
RETURN 1;
END IF;
ELSE
IF ar[n] < br[n] THEN
RETURN -1;
ELSIF ar[n] > br[n] THEN
RETURN 1;
END IF;
END IF;
n := n + 1;
END LOOP;
IF n > array_length(ar, 1) AND n > array_length(br, 1) THEN
RETURN 0;
ELSIF n > array_length(ar, 1) THEN
RETURN 1;
ELSE
RETURN -1;
END IF;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
You can then create an operator class so that sorting can be done by using the comparison function with ORDER BY field USING <#:
CREATE OR REPLACE FUNCTION vernum_lt(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) < 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_lte(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) <= 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_eq(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) = 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_gt(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) > 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION vernum_gte(a text, b text) RETURNS boolean AS $$
BEGIN
RETURN vercmp(a, b) >= 0;
END;
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE OPERATOR <# ( PROCEDURE = vernum_lt, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR ># ( PROCEDURE = vernum_gt, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR =# ( PROCEDURE = vernum_lte, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR <=# ( PROCEDURE = vernum_lte, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR >=# ( PROCEDURE = vernum_gte, LEFTARG = text, RIGHTARG = text);
CREATE OPERATOR CLASS vernum_ops FOR TYPE varchar USING btree AS
OPERATOR 1 <# (text, text),
OPERATOR 2 <=# (text, text),
OPERATOR 3 =#(text, text),
OPERATOR 4 >=# (text, text),
OPERATOR 5 ># (text, text),
FUNCTION 1 vercmp(text, text)
;
FİXED THİS WAY.
<pre>
00000001 1
00000001.00000001 1.1
00000001.00000001.00000001 1.1.1
00000001.00000002 1.2
00000001.00000009 1.9
00000001.00000010 1.10
00000001.00000011 1.11
00000001.00000012 1.12
00000002 2
00000002.00000001 2.1
00000002.00000001.00000001 2.1.1
00000002.00000002 2.2
00000002.00000009 2.9
00000002.00000010 2.10
00000002.00000011 2.11
00000002.00000012 2.12
select * from (select '000000001' as tCode,'1' as Code union
select '000000001.000000001' as tCode,'1.1'as Code union
select '000000001.000000001.000000001' as tCode,'1.1.1'as Code union
select '000000001.000000002' as tCode,'1.2' union
select '000000001.000000010' as tCode,'1.10'as Code union
select '000000001.000000011' as tCode,'1.11'as Code union
select '000000001.000000012' as tCode,'1.12'as Code union
select '000000001.000000009' as tCode,'1.9' as Code
union
select '00000002' as tCode,'2'as Code union
select '00000002.00000001' as tCode,'2.1'as Code union
select '00000002.00000001.00000001' as tCode,'2.1.1'as Code union
select '00000002.00000002' as tCode,'2.2'as Code union
select '00000002.00000010' as tCode,'2.10'as Code union
select '00000002.00000011' as tCode,'2.11'as Code union
select '00000002.00000012' as tCode,'2.12'as Code union
select '00000002.00000009' as tCode,'2.9'as Code ) as t
order by t.tCode
</pre>
<pre>
public static string GenerateToCodeOrder(this string code)
{
var splits = code.Split('.');
var codes = new List<string>();
foreach (var str in splits)
{
var newStr = "";
var zeroLength = 10 - str.Length;
for (int i = 1; i < zeroLength; i++)
{
newStr += "0";
}
newStr += str;
codes.Add(newStr);
}
return string.Join(".", codes);
}
</pre>
In M$ SQL I had issues with hierachyid with some data...
select Convert(hierarchyid, '/' + '8.3.0000.1088' + '/')
To get around this I used pasename (relies on '.' being the separator)...
Order by
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 1))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 2))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 3))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 4))),
convert(int, reverse (Parsename( reverse(tblSoftware.softwareVersion) , 5)))
If the column type for version is varchar the sorting is done as expected.
This is beacuse varchar is not padded by spaces.
Here is an ORACLE expression you can use in an ORDER BY:
select listagg(substr('0000000000' || column_value,-9), '.') within group(order by rownum) from xmltable(replace(version, '.',','))
assuming your version column has only dot as separator (any number of levels).
(if not, up to you to change the replace by e.g. translate(version, '.-', ',,'))
I would do as Joel Coehoorn said. Then to re-arrange your data structure you don't have to manually do it. You can write a simple script that will do the job for all 600 records.
Just remove the dots (Inline, replace with empty string) cast the result as int and order by the result. Works great:
a.Version = 1.4.18.14
select...
Order by cast( replace (a.Version,'.','') as int)