Calculating a checksum in SQL - sql

I have a database field of Brazilian CPF numbers and want to check for their validity. These are 11 digit strings which are 9 digits and 2 checksum digits.
I currently implemented the checksum in MS Excel (see below) but I'd like to figure out a way to do it in SQL.
Checksum works as follows: (Hold on tight, this is nuts.)
The CPF number is written in the form ABCDEFGHI / JK or directly as
ABCDEFGHIJK, where the digits can not all be the same as each other.
The J is called 1st digit check of the CPF number.
The K is called the 2nd check digit of the CPF number.
First digit (J):
Multiply each digit of the first 9 by a constant:
10*A + 9*B + 8*C + 7*D + 6*E + 5*F + 4*G + 3*H + 2*I
Divide this sum by 11 and if the remainder is 0 or 1, J will be 0. If the remainder is >=2, J will be 11 - remainder.
Second digit (K): (Same calculation but including digit J)
Multiply each digit of the first 10 by a constant:
11A + 10B + 9C + 8D + 7E + 6F + 5G + 4H + 3I + 2J
Divide this sum by 11 and if the remainder is 0 or 1, K will be 0. If the remainder is >=2, K will be 11 - remainder.
--Implementation in MS Excel--
Assuming the CPF is in A2.
Optimizations here are welcome but not really the point of this question.
Digit J: =IF(MOD(SUM(MID($A2,1,1)*10,MID($A2,2,1)*9,MID($A2,3,1)*8,MID($A2,4,1)*7,MID($A2,5,1)*6,MID($A2,6,1)*5,MID($A2,7,1)*4,MID($A2,8,1)*3,MID($A2,9,1)*2),11)<=1,NUMBERVALUE(LEFT(RIGHT($A2,2),1))=0,NUMBERVALUE(LEFT(RIGHT($A2,2),1))=(11-MOD(SUM(MID($A2,1,1)*10,MID($A2,2,1)*9,MID($A2,3,1)*8,MID($A2,4,1)*7,MID($A2,5,1)*6,MID($A2,6,1)*5,MID($A2,7,1)*4,MID($A2,8,1)*3,MID($A2,9,1)*2),11)))
Digit K:
=IF(MOD(SUM(MID($A2,1,1)*11,MID($A2,2,1)*10,MID($A2,3,1)*9,MID($A2,4,1)*8,MID($A2,5,1)*7,MID($A2,6,1)*6,MID($A2,7,1)*5,MID($A2,8,1)*4,MID($A2,9,1)*3,MID($A2,10,1)*2),11)<=1,NUMBERVALUE(LEFT(RIGHT($A2,1),1))=0,NUMBERVALUE(LEFT(RIGHT($A2,1),1))=(11-MOD(SUM(MID($A2,1,1)*11,MID($A2,2,1)*10,MID($A2,3,1)*9,MID($A2,4,1)*8,MID($A2,5,1)*7,MID($A2,6,1)*6,MID($A2,7,1)*5,MID($A2,8,1)*4,MID($A2,9,1)*3,MID($A2,10,1)*2),11)))

My test table:
-- Create a table called CPF
CREATE TABLE CPF(Id integer PRIMARY KEY, No integer);
-- Create few records in this table
INSERT INTO CPF VALUES(1, 12345678901);
My nested query:
SELECT No,
(CASE WHEN (J != J2) THEN 'J wrong!' ELSE 'J ok!' END) as Jchk,
(CASE WHEN (K != K2) THEN 'K wrong!' ELSE 'K ok!' END) as Kchk
FROM
(SELECT No, J, K,
(CASE WHEN MJ < 2 THEN 0 ELSE 11 - MJ END) as J2,
(CASE WHEN MK < 2 THEN 0 ELSE 11 - MK END) as K2
FROM
(SELECT No, J, K,
MOD(10*A + 9*B + 8*C + 7*D + 6*E + 5*F + 4*G + 3*H + 2*I, 11) as MJ,
MOD(11*A + 10*B + 9*C + 8*D + 7*E + 6*F + 5*G + 4*H + 3*I + 2*J, 11) as MK
FROM
(SELECT
No,
substr(to_char(No), 1, 1) as A,
substr(to_char(No), 2, 1) as B,
substr(to_char(No), 3, 1) as C,
substr(to_char(No), 4, 1) as D,
substr(to_char(No), 5, 1) as E,
substr(to_char(No), 6, 1) as F,
substr(to_char(No), 7, 1) as G,
substr(to_char(No), 8, 1) as H,
substr(to_char(No), 9, 1) as I,
substr(to_char(No), 10, 1) as J,
substr(to_char(No), 11, 1) as K
FROM CPF)))
;

Assuming you have a table with an id primary key column and a cpf column that is NUMBER(9,0) data type then something like:
WITH digits ( id, a, b, c, d, e, f, g, h, i ) AS (
SELECT id,
MOD( TRUNC( cpf / 1e8 ), 10 ),
MOD( TRUNC( cpf / 1e7 ), 10 ),
MOD( TRUNC( cpf / 1e6 ), 10 ),
MOD( TRUNC( cpf / 1e5 ), 10 ),
MOD( TRUNC( cpf / 1e4 ), 10 ),
MOD( TRUNC( cpf / 1e3 ), 10 ),
MOD( TRUNC( cpf / 1e2 ), 10 ),
MOD( TRUNC( cpf / 1e1 ), 10 ),
MOD( TRUNC( cpf / 1e0 ), 10 )
FROM your_table
),
values1 ( id, j, k ) AS (
SELECT id,
MOD( 10*A + 9*B + 8*C + 7*D + 6*E + 5*F + 4*G + 3*H + 2*I, 11 ),
11*A + 10*B + 9*C + 8*D + 7*E + 6*F + 5*G + 4*H + 3*I
FROM digits
),
values2 ( id, j, k ) AS (
SELECT id,
CASE WHEN j <= 1 THEN 0 ELSE 11 - j END,
MOD( k + 2 * CASE WHEN j <= 1 THEN 0 ELSE 11 - j END, 11 )
FROM values1
)
SELECT id,
j,
CASE WHEN k <= 1 THEN 0 ELSE 11 - k END AS k
FROM values2

#SAR622: great question and thanks for the algorithm.
Here is a t-SQL solution for SQL Server, just in case. Note that Cadastro de Pessoas Físicas (CPF) numbers can only have 11 digits (pre-panded by zeros), that is they cannot exceed 10^12-1. If you note 14 digit numbers in your dataset, these are likely to be Cadastro Nacional da Pessoa Jurídica (CNPJ) numbers issued to business (or typos or something else). The fake CPF and CNPJ numbers can be generated (in bulk) and validated (individually) here. Also this site provides more info about a business located by its CNPJ (think of it as an implicit CNPJ validation). When validating a CPF number remember to check if the number is in range [0, 10^12-1]. You may need to remove any punctuation symbols and other invalid characters (as users, we tend to make typos).
This input table has top 5 invalid CPF numbers and bottom 4 valid ones:
IF OBJECT_ID('tempdb..#x') IS NOT NULL DROP TABLE #x;
CREATE TABLE #x (CPF BIGINT default NULL);
INSERT INTO #x (CPF) VALUES (12345678900);
INSERT INTO #x (CPF) VALUES (11);
INSERT INTO #x (CPF) VALUES (1010101010101010);
INSERT INTO #x (CPF) VALUES (11111179011525590);
INSERT INTO #x (CPF) VALUES (-32081397641);
INSERT INTO #x (CPF) VALUES (00000008726210061);
INSERT INTO #x (CPF) VALUES (56000608314);
INSERT INTO #x (CPF) VALUES (73570630706);
INSERT INTO #x (CPF) VALUES (93957133564);
The following t-SQL function modularizes implementation, but will likely be slower than the raw t-SQL that follows. Alternatively, you can create a t-SQL function with a TABLE input/output or a stored procedure.
ALTER FUNCTION fnIsCPF(#n BIGINT) RETURNS INT AS
BEGIN
DECLARE #isValid BIT = 0;
IF (#n > 0 AND #n < 100000000000)
BEGIN
--Parse out numbers
DECLARE #a TINYINT = FLOOR( #n / 10000000000)% 10;
DECLARE #b TINYINT = FLOOR( #n / 1000000000)% 10;
DECLARE #c TINYINT = FLOOR( #n / 100000000)% 10;
DECLARE #d TINYINT = FLOOR( #n / 10000000)% 10;
DECLARE #e TINYINT = FLOOR( #n / 1000000)% 10;
DECLARE #f TINYINT = FLOOR( #n / 100000)% 10;
DECLARE #g TINYINT = FLOOR( #n / 10000)% 10;
DECLARE #h TINYINT = FLOOR( #n / 1000)% 10;
DECLARE #i TINYINT = FLOOR( #n / 100)% 10;
DECLARE #j TINYINT = ISNULL(NULLIF(NULLIF(11-( 10*#a + 9*#b + 8*#c + 7*#d + 6*#e + 5*#f + 4*#g + 3*#h + 2*#i) % 11, 11), 10), 0);
DECLARE #k TINYINT = ISNULL(NULLIF(NULLIF(11 - (11*#a +10*#b + 9*#c + 8*#d + 7*#e + 6*#f + 5*#g + 4*#h + 3*#i + 2 * #j)% 11, 11), 10), 0);
RETURN CASE WHEN #j=FLOOR(#n / 10)% 10 AND #k=FLOOR(#n)% 10 THEN 1 ELSE 0 END
END;
RETURN #isValid;
END;
The output is:
SELECT CPF, isValid=dbo.fnIsCPF(CPF) FROM #x
CPF isValid
12345678900 0
11 0
1010101010101010 0
11111179011525590 0
-32081397641 0
8726210061 1
56000608314 1
73570630706 1
93957133564 1
t-SQL for a table:
WITH digits ( CPF, a, b, c, d, e, f, g, h, i ) AS (
SELECT CPF,
FLOOR( CPF / 10000000000)% 10,
FLOOR( CPF / 1000000000)% 10,
FLOOR( CPF / 100000000)% 10,
FLOOR( CPF / 10000000)% 10,
FLOOR( CPF / 1000000)% 10,
FLOOR( CPF / 100000)% 10,
FLOOR( CPF / 10000)% 10,
FLOOR( CPF / 1000)% 10,
FLOOR( CPF / 100)% 10
FROM #x
),
jk ( CPF, j, k ) AS (
SELECT CPF, ISNULL(NULLIF(NULLIF(11-( 10*A + 9*B + 8*C + 7*D + 6*E + 5*F + 4*G + 3*H + 2*I) % 11, 11), 10), 0),
11*A +10*B + 9*C + 8*D + 7*E + 6*F + 5*G + 4*H + 3*I
FROM digits
),
jk2 ( CPF, j, k ) AS (
SELECT CPF, j, ISNULL(NULLIF(NULLIF(11 - (k + 2 * j)% 11, 11), 10), 0)
FROM jk
)
SELECT CPF, isValid=CASE WHEN CPF>0 AND CPF<99999999999 AND j=FLOOR( CPF / 10)% 10 AND k=FLOOR( CPF)% 10 THEN 1 ELSE 0 END
FROM jk2
yielding the same output.

Related

SQL Server formatting negative values from selected data

I am new to stackoverflow but I do search it often.
I am creating a report from data in which I have to format negative numbers like,
-00000010 (9 characters max)
I am getting this,
000000-10
This is what I am attempting now but I'm having issues. Any help would be greatly appreciated.
SELECT 'H'
+ DG.BLOCK
+ LEFT(DG.ZIP,5)
+ RIGHT('000000000'
+ CAST(CAST(SUM(DG.WP)AS INT) AS VARCHAR(9)),9)
+ RIGHT('000000000' + CAST(CAST(SUM(DG.WE)AS INT) AS VARCHAR(9)),9)
+ RIGHT('000000000' + CAST(CAST(SUM(DG.EP)AS INT) AS VARCHAR(9)),9)
+ RIGHT('000000000' + CAST(CAST(SUM(DG.EE)AS INT) AS VARCHAR(9)),9)
+ RIGHT('000000000' + CAST(CAST(COUNT(DGG.CLAIMCONTROL)AS INT) AS VARCHAR(9)),9)
+ RIGHT('000000000' + CAST(CAST(SUM(DGG.INC) AS INT) AS VARCHAR(9)),9)
+ RIGHT('000000000' + CAST(CAST(SUM(DGG.PAID)AS INT) AS VARCHAR(9)),9)
+ RIGHT('000000000' + CAST(CAST(SUM(DGG.ALAE) AS INT) AS VARCHAR(9)),9)
AS [H Record]
FROM TABLE
If 2012+, you have the option of Format().
Example
Select replace(format(-1,' 000000000'),'- ','-')
Returns
-000000001 If number was negative
000000001 If number was positive
Just a word of caution. Format() has some great functionality, but is not known to be a high performer.
The following code demonstrates formatting the data as either 9 digits plus an optional sign or as a fixed 9 characters including the sign.
-- Sample data.
declare #Samples as Table ( Sample Int );
insert into #Samples ( Sample ) values ( 0 ), ( 1 ), ( -10 ), ( 100 ), ( -1000 );
-- Format the data.
select Sample,
case when Sign( Sample ) = -1 then '-' else '' end +
Right( Replicate( '0', 8 ) + Cast( Abs( Sample ) as VarChar(9) ), 9 ) as FormattedSample9PlusSign,
case when Sign( Sample ) = -1 then
'-' + Right( Replicate( '0', 7 ) + Cast( -Sample as VarChar(8) ), 8 ) else
Right( Replicate( '0', 8 ) + Cast( Sample as VarChar(9) ), 9 ) end as FormattedSample9
from #Samples;
Tip: In SSMS use query results to text (Ctrl-T) for more convenient display.
You can try this, if you do not have v2012+:
DECLARE #mockup TABLE(SomeNumber INT);
DECLARE #padWidth INT=3;
INSERT INTO #mockup VALUES(-1000),(-500),(-1),(0),(1),(500),(1000);
SELECT CASE WHEN m.SomeNumber < 0 THEN '-' ELSE ' ' END
+ REPLACE(STR(ABS(m.SomeNumber),#padWidth),' ','0')
FROM #mockup AS m;
Numbers, which are to big, will be returned as ***. This is better than other approaches cutting the string with RIGHT or LEFT. They might return a bad result...
This is returned
-***
-500
-001
000
001
500
***
In DB2 this works to get a number of 15 digits including the sign:
CASE WHEN MYNUMBER < 0 THEN '-' || LPAD(MYNUMBER , 14, 0)
ELSE LPAD(MYNUMBER , 15, 0)
END

Convert Recursive CTE to Recursive Subquery

How would I convert the following CTE into a recursive subquery? It's an implementation of Newtons Method.
Reasons:
1) I have no permissions to create functions or stored procs in the DB
2) I must do everything in TSQL
3) Not using Oracle
TESTDATA Table
PMT t V
6918.26 6 410000
3636.51 14 460000
3077.98 22 630000
1645.14 18 340000
8591.67 13 850000
Desired Output
PMT t V Newton
6918.26 6 410000 0.066340421
3636.51 14 460000 0.042449138
3077.98 22 630000 0.024132674
1645.14 18 340000 0.004921588
8591.67 13 850000 0.075982984
_
DECLARE #PMT AS FLOAT
DECLARE #t AS FLOAT
DECLARE #V AS FLOAT
--These will be only for 1 example.
SET #PMT = 6918.26740930922
SET #t = 6
SET #V = 410000
;With Newton (n, i,Fi,dFi) AS (
--base
SELECT
1,
CAST(0.1 AS FLOAT)
,#PMT * (1 - POWER((1 + CAST(0.1 AS FLOAT) / 12), (-#t * 12))) - #V * CAST(0.1 AS FLOAT) / 12
,#PMT * #t * 12 * POWER((1 + CAST(0.1 AS FLOAT) / 12), (-#t * 12 - 1)) - #V
UNION ALL
--recursion
SELECT
n + 1
,i - Fi/dFi
,#PMT * (1 - POWER((1 + i / 12), (-#t * 12))) - #V * i / 12
,#PMT * #t * 12 * POWER((1 + i / 12), (-#t * 12 - 1)) - #V
FROM Newton WHERE n < 500)
--to get the desired value for params above
SELECT [x].i
FROM (
SELECT n, i, Fi, dFi
FROM Newton
WHERE n = 500
) [x]
OPTION (MAXRECURSION 500)
_
I want Newton to evaluate on Every record of TestData as a stand alone column.
Any thoughts?

SQL Server 2008 - Query to get result in fraction format

I have a table which contains data like this:
MinFormat(int) MaxFormat(int) Precision(nvarchar)
-2 3 1/2
The values in precision can be 1/2, 1/4, 1/8, 1/16, 1/32, 1/64 only.
Now I want result from query as -
-2
-3/2
-1
-1/2
0
1/2
1
3/2
2
5/2
3
Any query to get the result as follows?
Idea is to create result based onMinimum boundary (MinFomrat col value which is integer) to Maximum boundary (MaxFormat Col value which is integer) accordingly to the precision value.
Hence, in above example, value should start from -2 and generate the next values based on the precision value (1/2) till it comes to 3
Note this will only work for Precision 1/1, 1/2, 1/4, 1/8, 1/16, 1/32 and 1/64
DECLARE #t table(MinFormat int, MaxFormat int, Precision varchar(4))
INSERT #t values(-2, 3, '1/2')
DECLARE #numerator INT, #denominator DECIMAL(9,7)
DECLARE #MinFormat INT, #MaxFormat INT
-- put a where clause on this to get the needed row
SELECT #numerator = 1,
#denominator = STUFF(Precision, 1, charindex('/', Precision), ''),
#MinFormat = MinFormat,
#MaxFormat = MaxFormat
FROM #t
;WITH N(N)AS
(SELECT 1 FROM(VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1))M(N)),
tally(N)AS(SELECT ROW_NUMBER()OVER(ORDER BY N.N)FROM N,N a,N b,N c,N d,N e,N f)
SELECT top(cast((#MaxFormat- #MinFormat) / (#numerator/#denominator) as int) + 1)
CASE WHEN val % 1 = 0 THEN cast(cast(val as int) as varchar(10))
WHEN val*2 % 1 = 0 THEN cast(cast(val*2 as int) as varchar(10)) + '/2'
WHEN val*4 % 1 = 0 THEN cast(cast(val*4 as int) as varchar(10)) + '/4'
WHEN val*8 % 1 = 0 THEN cast(cast(val*8 as int) as varchar(10)) + '/8'
WHEN val*16 % 1 = 0 THEN cast(cast(val*16 as int) as varchar(10)) + '/16'
WHEN val*32 % 1 = 0 THEN cast(cast(val*32 as int) as varchar(10)) + '/32'
WHEN val*64 % 1 = 0 THEN cast(cast(val*64 as int) as varchar(10)) + '/64'
END
FROM tally
CROSS APPLY
(SELECT #MinFormat +(N-1) *(#numerator/#denominator) val) x
Sorry, now I'm late, but this was my approach:
I'd wrap this in a TVF actually and call it like
SELECT * FROM dbo.FractalStepper(-2,1,'1/4');
or join it with your actual table like
SELECT *
FROM SomeTable
CROSS APPLY dbo.MyFractalSteller(MinFormat,MaxFormat,[Precision]) AS Steps
But anyway, this was the code:
DECLARE #tbl TABLE (ID INT, MinFormat INT,MaxFormat INT,Precision NVARCHAR(100));
--Inserting two examples
INSERT INTO #tbl VALUES(1,-2,3,'1/2')
,(2,-4,-1,'1/4');
--Test with example 1, just set it to 2 if you want to try the other example
DECLARE #ID INT=1;
--If you want to get your steps numbered, just de-comment the tree occurencies of "Step"
WITH RecursiveCTE as
(
SELECT CAST(tbl.MinFormat AS FLOAT) AS RunningValue
,CAST(tbl.MaxFormat AS FLOAT) AS MaxF
,1/CAST(SUBSTRING(LTRIM(RTRIM(tbl.Precision)),3,10) AS FLOAT) AS Prec
--,1 AS Step
FROM #tbl AS tbl
WHERE tbl.ID=#ID
UNION ALL
SELECT RunningValue + Prec
,MaxF
,Prec
--,Step + 1
FROM RecursiveCTE
WHERE RunningValue + Prec <= MaxF
)
SELECT RunningValue --,Step
,CASE WHEN CAST(RunningValue AS INT)<>RunningValue
THEN CAST(RunningValue / Prec AS VARCHAR(10)) + '/' + CAST(CAST(1/Prec AS INT) AS VARCHAR(MAX))
ELSE CAST(RunningValue AS VARCHAR(10))
END AS RunningValueFractal
FROM RecursiveCTE;
The result
Value ValueFractal
-2 -2
-1,5 -3/2
-1 -1
-0,5 -1/2
0 0
0,5 1/2
1 1
1,5 3/2
2 2
2,5 5/2
3 3
Mine is a bit different others. I perform the addition on fraction and at the end, simplified the fraction.
-- This solution uses CTE
-- it breaks the #min, #max number into fraction
-- perform the addition in terms of fraction
-- at result, it attemp to convert the fraction to simpliest form
declare #min int,
#max int,
#step varchar(10),
#step_n int, -- precision step numerator portion
#step_d int -- precision step denominator portion
select #min = -2,
#max = 3,
#step = '1/16'
select #step_n = left(#step, charindex('/', #step) - 1),
#step_d = stuff(#step, 1, charindex('/', #step), '')
; with rcte as
(
-- Anchor member
select n = #min, -- numerator
d = 1, -- denominator
v = convert(decimal(10,5), #min)
union all
-- Recursive member
select n = case when ( (r.n * #step_d) + (r.d * #step_n) ) % #step_d = 0
and (r.d * #step_d) % #step_d = 0
then ( (r.n * #step_d) + (r.d * #step_n) ) / #step_d
else (r.n * #step_d) + (r.d * #step_n)
end,
d = case when ( (r.n * #step_d) + (r.d * #step_n) ) % #step_d = 0
and (r.d * #step_d) % #step_d = 0
then (r.d * #step_d) / #step_d
else (r.d * #step_d)
end,
v = convert(decimal(10,5), ((r.n * #step_d) + (r.d * #step_n)) / (r.d * #step_d * 1.0))
from rcte r
where r.v < #max
)
select *,
fraction = case when n = 0
then '0'
when coalesce(d2, d) = 1
then convert(varchar(10), coalesce(n2, n))
else convert(varchar(10), coalesce(n2, n)) + '/' + convert(varchar(10), coalesce(d2, d))
end
from rcte r
cross apply -- use to simplify the fraction result
(
select n2 = case when n % 32 = 0 and d % 32 = 0 then n / 32
when n % 16 = 0 and d % 16 = 0 then n / 16
when n % 8 = 0 and d % 8 = 0 then n / 8
when n % 4 = 0 and d % 4 = 0 then n / 4
when n % 2 = 0 and d % 2 = 0 then n / 2
end,
d2 = case when n % 32 = 0 and d % 32 = 0 then d / 32
when n % 16 = 0 and d % 16 = 0 then d / 16
when n % 8 = 0 and d % 8 = 0 then d / 8
when n % 4 = 0 and d % 4 = 0 then d / 4
when n % 2 = 0 and d % 2 = 0 then d / 2
end
) s
order by v
option (MAXRECURSION 0)

Converting CHAR string to nth letter in Alphabet string in SQL

I have to build a process that takes a VARCHAR string (for example 'AHT559') and converts it to a INT only string by converting the Alphabetic chars to INTEGERS based on the nth letter in the alphabet. The above would thus result in: 010820559.
I have done this in SAS before, but I'm relatively new to SQL. What would be the best way to do this in SQL?
Here is what I've done in SAS:
DO _i = 1 TO length( account );
IF (rank( char( account, _i ) ) -64) < 0 THEN agreement_hash = CATS( agreement_hash, char( account, _i ) );
ELSE IF (rank( char( account, _i ) ) -64) < 10 THEN agreement_hash = CATS( agreement_hash, 0, rank( char( account, _i ) )-64 );
ELSE agreement_hash = CATS( agreement_hash, rank( char( account, _i ) )-64 );
END;
If the format of the values is always the same as you state in the comments and you only need to process a single value at a time you can do some simple string manipulation to convert the characters to integers using their ASCII values, and subtracting 64 to get the number of the alphabetic character:
SELECT ASCII('A') -- produces 65
SELECT ASCII('A') - 64 -- produces 1
This is a little long winded and could be done in less lines of code, but it's separated for clarity.
DECLARE #val NVARCHAR(10) = 'AHT559'
-- get first, second and third character numeric values
DECLARE #first INT = ASCII(SUBSTRING(#val, 1, 1)) - 64
DECLARE #second INT = ASCII(SUBSTRING(#val, 2, 1)) - 64
DECLARE #third INT = ASCII(SUBSTRING(#val, 3, 1)) - 64
-- join them together adding a '0' if < 10
SELECT RIGHT('0' + CAST(#first AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST(#second AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST(#third AS VARCHAR(2)), 2)
+ RIGHT(#val, 3)
Tested on 4 million rows:
-- temp table creation - takes approx 100 seconds on my machine
CREATE TABLE #temp (val NVARCHAR(6))
DECLARE #rowno INT = 1
SELECT #rowno = 1
WHILE #rowno <= 4000000
BEGIN
INSERT INTO #temp ( val ) VALUES ( 'AHT559' )
SELECT #rowno = #rowno + 1
END
To run this code against the entire temp table takes < 20 seconds on my machine:
SELECT val AS OrignalValue,
RIGHT('0' + CAST( ASCII(SUBSTRING(val, 1, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST( ASCII(SUBSTRING(val, 2, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST( ASCII(SUBSTRING(val, 3, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT(val, 3) AS FormattedValue
FROM #temp
Here is a similar script for sqlserver, any character which is not a capital letter is assumed a digit in this syntax:
DECLARE #x varchar(100) = 'AHT559'
DECLARE #p int = len(#x)
WHILE #p > 0
SELECT #x =
CASE WHEN substring(#x, #p, 1) between 'A' and 'Z'
THEN stuff(#x, #p, 1, right(ascii(substring(#x, #p, 1)) - 64 + 100, 2))
ELSE #x END,
#p -= 1
SELECT #x
Result:
010820559
You could use something like the below, possibly as a scalar function to do this conversion.
DECLARE #i INT
DECLARE #Item NVARCHAR(4000) = 'AHT1234'
DECLARE #ItemTable TABLE
(
Item NCHAR(1)
)
SET #i = 1
--Split the input string into separate characters, store in temp table
WHILE (#i <= LEN(#Item))
BEGIN
INSERT INTO #ItemTable(Item)
VALUES(SUBSTRING(#Item, #i, 1))
SET #i = #i + 1
END
DECLARE #AlphaTable TABLE (
Letter NCHAR(1),
Position NVARCHAR(2)
)
-- Populate this with the whole alphabet obviously. Could be a permanent rather than temp table.
INSERT INTO #AlphaTable
( Letter, Position )
VALUES ( N'A', '01'),
(N'H', '08'),
(N'T', '20')
DECLARE #Output NVARCHAR(50)
-- Convert the output and concatenate it back to a single output.
SELECT #Output = COALESCE(#output, '') + Converted
FROM (
SELECT CASE WHEN ISNUMERIC(Item) = 1
THEN CONVERT(NVARCHAR(1), Item)
ELSE (SELECT Position FROM #AlphaTable WHERE Letter = CONVERT(NCHAR(1), Item))
END AS Converted
FROM #ItemTable
) AS T1
SELECT #Output
GO
Try this.
DECLARE #STR VARCHAR(MAX)= 'AHT559',
#SP INT,
#SP_STR VARCHAR(50),
#OUTPUT VARCHAR(MAX)=''
DECLARE #TEMP_STR VARCHAR(50)
SET #TEMP_STR = #STR
WHILE Patindex('%[A-Z]%', #TEMP_STR) <> 0
BEGIN
SELECT #SP = Patindex('%[A-Z]%', #TEMP_STR)
SELECT #SP_STR = Upper(LEFT(#TEMP_STR, #SP))
SELECT #SP_STR = ( Ascii(#SP_STR) - 65 ) + 1
SELECT #TEMP_STR = Stuff(#TEMP_STR, 1, #SP, '')
SET #OUTPUT += RIGHT('0' + #SP_STR, 2)
END
SELECT #OUTPUT + Substring(#STR, Patindex('%[0-9]%', #STR), Len(#STR))
How about using a CTE to create every combination of the first 3 letters and using that to match to:
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE Accounts
(
Account VARCHAR(6)
)
INSERT INTO Accounts
VALUES ('AHT559'), ('BXC556'),
('CST345')
Query 1:
;WITH AlphaToNum
AS
(
SELECT *
FROM (VALUES
('A', '01'), ('B', '02'), ('C', '03'), ('D', '04'),
('E', '05'), ('F', '06'), ('G', '07'), ('H', '08'),
('I', '09'), ('J', '10'), ('K', '11'), ('L', '12'),
('M', '13'), ('N', '14'), ('O', '15'), ('P', '16'),
('Q', '17'), ('R', '18'), ('S', '19'), ('T', '20'),
('U', '21'), ('V', '22'), ('W', '23'), ('X', '24'),
('Y', '25'), ('Z', '26')
) X(alpha, num)
),
MappingTable
As
(
SELECT A1.alpha + A2.alpha + A3.alpha as match, A1.num + A2.num + A3.num as val
FROM AlphaToNum A1
CROSS APPLY AlphaToNum A2
CROSS APPLY AlphaToNum A3
)
SELECT A.Account, M.val + SUBSTRING(A.Account,4, 3) As ConvertedAccount
FROM MappingTable M
INNER JOIN Accounts A
ON LEFT(A.Account,3) = M.match
Results:
| Account | ConvertedAccount |
|---------|------------------|
| AHT559 | 010820559 |
| BXC556 | 022403556 |
| CST345 | 031920345 |
This is probably best done using a CLR UDF, but a full answer is too long for this format.
Basically you need to create a UDF (User defined function) that takes a string (nvarchar...) as an input and returns a string as an output. You can do that with C# quite easily, and you need to wrap it with the CLR integration requirements.
You can see here for relevant information.
The code could look something like:
[Microsoft.SqlServer.Server.SqlFunction(
IsDeterministic=true,
IsPrecise=true,
SystemDataAccess=SystemDataAccessKind.None)]
public static SqlString ToNthAlpha(SqlString value)
{
if(value.IsNull)
return value;
char []chars = value.Value.ToCharArray();
StringBuilder res = new StringBuilder();
for(int i = 0; i < chars.Length; i++)
{
if(chars[i] >= 'A' && chars[i] <= 'Z')
res.AppendFormat("{0:00}", chars[i] - 'A');
res.Append(chars[i]);
}
return new SqlString(res.ToString());
}

How to create ordinal numbers (i.e. "1st" "2nd", etc.) in SQL Server

I recently responded to this question in the SSRS-2008 tag that required changing the day number in a date to the ordinal number (i.e. "1st", "2nd" instead of "1", "2"). The solution involved a VB.Net function. I'm curious how one would go about performing this task in SQL (t-sql and SQL Server in particular), or if there is some built in support.
So here is a scenario: say you have organized a footrace for 1000 runners and have the results in a table with the columns Name and Place (in normal numbers). You want to create a query that will display a user's name and their place in ordinal numbers.
Here's a scalable solution that should work for any number. I thought other's used % 100 for 11,12,13 but I was mistaken.
WITH CTE_Numbers
AS
(
SELECT 1 num
UNION ALL
SELECT num + 1
FROM CTE_Numbers
WHERE num < 1000
)
SELECT CAST(num AS VARCHAR(10))
+
CASE
WHEN num % 100 IN (11,12,13) THEN 'th' --first checks for exception
WHEN num % 10 = 1 THEN 'st'
WHEN num % 10 = 2 THEN 'nd'
WHEN num % 10 = 3 THEN 'rd'
ELSE 'th' --works for num % 10 IN (4,5,6,7,8,9,0)
END
FROM CTE_Numbers
OPTION (MAXRECURSION 0)
You can do that just as easily in SQL as in the app layer:
DECLARE #myDate DATETIME = '2015-05-21';
DECLARE #day INT;
SELECT #day = DAY(#myDate);
SELECT CASE WHEN #day IN ( 11, 12, 13 ) THEN CAST(#day AS VARCHAR(10)) + 'th'
WHEN #day % 10 = 1 THEN CAST(#day AS VARCHAR(10)) + 'st'
WHEN #day % 10 = 2 THEN CAST(#day AS VARCHAR(10)) + 'nd'
WHEN #day % 10 = 3 THEN CAST(#day AS VARCHAR(10)) + 'rd'
ELSE CAST(#day AS VARCHAR(10)) + 'th'
END
You could also put this in a scalar function if necessary.
EDIT
For your example, it would be:
SELECT Name ,
CASE WHEN Place IN ( 11, 12, 13 )
THEN CAST(Place AS VARCHAR(10)) + 'th'
WHEN Place % 10 = 1 THEN CAST(Place AS VARCHAR(10)) + 'st'
WHEN Place % 10 = 2 THEN CAST(Place AS VARCHAR(10)) + 'nd'
WHEN Place % 10 = 3 THEN CAST(Place AS VARCHAR(10)) + 'rd'
ELSE CAST(Place AS VARCHAR(10)) + 'th'
END AS Place
FROM FootRaceResults;
Be very afraid:
with
ArabicRomanConversions as (
select *
from ( values
( 0, '', '', '', '' ), ( 1, 'I', 'X', 'C', 'M' ), ( 2, 'II', 'XX', 'CC', 'MM' ), ( 3, 'III', 'XXX', 'CCC', 'MMM' ), ( 4, 'IV', 'XL', 'CD', '?' ),
( 5, 'V', 'L', 'D', '?' ), ( 6, 'VI', 'LX', 'DC', '?' ), ( 7, 'VII', 'LXX', 'DCC', '?' ), ( 8, 'VIII', 'LXXX', 'DCCC', '?' ), ( 9, 'IX', 'XC', 'CM', '?' )
) as Placeholder ( Arabic, Ones, Tens, Hundreds, Thousands )
),
OrdinalConversions as (
select *
from ( values
( 1, 'st' ), ( 2, 'nd' ), ( 3, 'rd' ), ( 11, 'th' ), ( 12, 'th' ), ( 13, 'th' )
) as Placeholder2 ( Number, Suffix )
),
Numbers as (
select 1 as Number
union all
select Number + 1
from Numbers
where Number < 3999 )
select Number as Arabic,
( select Thousands from ArabicRomanConversions where Arabic = Number / 1000 ) +
( select Hundreds from ArabicRomanConversions where Arabic = Number / 100 % 10 ) +
( select Tens from ArabicRomanConversions where Arabic = Number / 10 % 10 ) +
( select Ones from ArabicRomanConversions where Arabic = Number % 10 ) as Roman,
Cast( Number as VarChar(4) ) + Coalesce( (
select top 1 Suffix from OrdinalConversions where Number = Numbers.Number % 100 or Number = Numbers.Number % 10 order by Number desc ), 'th' ) as Ordinal
from Numbers option ( MaxRecursion 3998 );
You could use a case statement, I.e.,
UPDATE: Taking into account the teens, as mentioned by TPhe and refactored slightly.
SELECT
Name,
CASE
WHEN Place in(11, 12, 13) then CAST(Place as VARCHAR(20)) + 'th'
WHEN RIGHT(CAST(Place as VARCHAR(20)), 1) = '1' then CAST(Place as VARCHAR(20)) + 'st'
WHEN RIGHT(CAST(Place as VARCHAR(20)), 1) = '2' then CAST(Place as VARCHAR(20)) + 'nd'
WHEN RIGHT(CAST(Place as VARCHAR(20)), 1) = '3' then CAST(Place as VARCHAR(20)) + 'rd'
ELSE CAST(Place as VARCHAR(20)) + 'th'
END as Place
FROM
RunnerTable
DECLARE #Number int = 94
SELECT
CONVERT(VARCHAR(10),#NUMBER) + CASE WHEN #Number % 100 IN (11, 12, 13) THEN 'th'
ELSE
CASE #Number % 10
WHEN 1 THEN 'st'
WHEN 2 THEN 'nd'
WHEN 3 THEN 'rd'
ELSE 'th'
END
END
This Would be much better for any number
create Function dbo.fn_Numbers_Ordinal (#N as bigint) returns varchar(50)
as Begin
Declare #a as varchar(50)= CAST(#N AS VARCHAR(50))
return(
SELECT CAST(#N AS VARCHAR(50))
+
CASE
WHEN Right(#a,2)='11' or Right(#a,2)='12' or Right(#a,2)='13' Then 'th'
WHEN #N % 10 = 1 THEN 'st'
WHEN #N % 10 = 2 THEN 'nd'
WHEN #N % 10 = 3 THEN 'rd'
ELSE 'th' --for #N % 10 IN (4,5,6,7,8,9,0)
END
)
end
Just figured I would add onto the various options. Here's a one-liner. I left this as a comment about a year ago. But someone suggested I submit it as an answer. So here ya go:
SELECT OrdinalRank = CONCAT(num, IIF(num % 100 IN (11,12,13),'th',COALESCE(CHOOSE(num % 10,'st','nd','rd'),'th')))
FROM (
VALUES (1),(2),(3),(4),(5),(10),(11),(20),(21),(22),(23),(24),(101),(102),(103)
) x(num)
--Result:
--1st
--2nd
--3rd
--4th
--5th
--10th
--11th
--20th
--21st
--22nd
--23rd
--24th
--101st
--102nd
--103rd
This takes advantage of the IIF and CHOOSE functions, which are only available in SQL 2012+.
DECLARE #Number int = 113,
#Superscript int
IF #Number IS NOT NULL
BEGIN
IF LEN(#Number) >= 2
SELECT #Superscript = RIGHT(#Number, 2)
ELSE
SELECT #Superscript = RIGHT(#Number, 1)
SELECT #Number as Number,
CASE WHEN #Superscript in (11,12,13) THEN 'th'
ELSE CASE WHEN #Superscript = 1 THEN 'st'
WHEN #Superscript = 2 THEN 'nd'
WHEN #Superscript = 3 THEN 'rd'
ELSE 'th'
END
END as Superscript
END ELSE
SELECT 0 as Number, 'th' as Superscript
Another option to solve this with the FORMAT function (also you can display month names in other languages):
;WITH cte AS (
SELECT 1 AS dayordinal ,'st' AS suffix
UNION
SELECT 2 AS dayordinal ,'nd' AS suffix
UNION
SELECT 3 AS dayordinal ,'rd' AS suffix
)
, YourTable AS --this is just for example
(SELECT CAST('1/1/2022' AS DATE) DateColumn
UNION
SELECT CAST('1/14/2022' AS DATE) DateColumn
UNION
SELECT CAST('4/4/2022' AS DATE) DateColumn
UNION
SELECT CAST('2/2/2022' AS DATE) DateColumn
UNION
SELECT CAST('3/13/2022' AS DATE) DateColumn
)
SELECT CAST(DATEPART(DAY, DateColumn) AS NVARCHAR(2))+ISNULL(c.suffix, 'th')+' '+ FORMAT(DateColumn, 'MMMM yyyy', 'fr-FR')
FROM YourTable t
LEFT JOIN cte c ON c.dayordinal=RIGHT(DATEPART(DAY, DateColumn),1)
Use the SSRS expression below:
= DAY(Globals!ExecutionTime) &
SWITCH(
DAY(Globals!ExecutionTime)= 1 OR DAY(Globals!ExecutionTime) = 21 OR DAY(Globals!ExecutionTime)=31, "st",
DAY(Globals!ExecutionTime)= 2 OR DAY(Globals!ExecutionTime) = 22 , "nd",
DAY(Globals!ExecutionTime)= 3 OR DAY(Globals!ExecutionTime) = 23 , "rd",
true, "th"
)
This is easy to implement and works well.
Public Function OrdinalNumberSuffix(ByVal InNumber As Integer) As String
Dim StrNumber As String, _
Digit As Byte, _
Suffix As String
StrNumber = Trim(Str(InNumber))
If Val(StrNumber) > 3 And Val(StrNumber) < 14 Then
Digit = Val(Right(StrNumber, 2))
Else
Digit = Val(Right(StrNumber, 1))
End If
Select Case Digit
Case 1: Suffix = "st"
Case 2: Suffix = "nd"
Case 3: Suffix = "rd"
Case Else: Suffix = "th"
End Select
OrdinalNumberSuffix = " " & StrNumber & Suffix & " "
End Function