SQL How to convert roman number to numeral vice versa - mysql

I would like to convert substring in roman to numeral, vice versa.
The input data would be some kind like this:
Input Output
Satu I Satu 1
Dua II Dua 2
Tiga Iii Tiga 3
empat Iv empat 4
lima v lima 5
enam VI enam 6
Tujuh 7 Tujuh VII
delapan 8 Delapan VIII
Sembilan belas Ix Sembilan belas 19
dua puluh xx dua puluh 20
Dua Satu xxi Dua Satu 21
No numeral No numeral
No roman No roman
Roman substring has inconsistent format (uppercase/lowercase/title case)
Is there a simple way to do it?
Or else (second questions), how to get the last substring from string (I, II, Iii, Iv, v, VI, 7, 8, Ix, xx, xxi) ?
I am using MySQL.
Thanks in advance.

If you find to find the last substring , do this:
SELECT REVERSE(LEFT(REVERSE(INPUT), LOCATE(' ',REVERSE(INPUT))));
I tested it with one of your values: Check here

Try creating a temporary table where it Convert Suffix or Roman Numerals to numbers.
INSERT INTO #tmpSuffixName(
sufCode,
sufLetter)
VALUES
('00',' '),('01','Sr.'),('02','Jr.'),('03','I'),('04','II'),
('05','III'),('06','IV'),('07','V'),('08','VI'),('09','VII'),
('10','VIII'),('11','IX'),('12','X'),('13','Sr. I'),('14','Sr. II'),
('15','Sr. III'),('16','Sr. IV'),('17','Sr. V'),('18','Sr. VI'),
('19','Sr. VII'),('20','Sr. VIII'),('21','Sr. IX'),('22','Sr. X'),
('23','Jr. I'),('24','Jr. II'),('25','Jr. III'),('26','Jr. IV'),
('27','Jr. V'),('28','Jr. VI'),('29','Jr. VII'),('30','Jr. VIII'),
('31','Jr. IX'),('32','Jr. X')
And you can execute a query like this:
SELECT
#mstrSuffix = sufCode
FROM #tmpCustomer
INNER JOIN #tmpSuffixName
ON ISNULL(tmpSuffixName, ' ') = sufLetter
WHERE ID = #GetID

CREATE FUNCTION dbo.uf_NumberToRoman(#NUMBER INT)
RETURNS VARCHAR(128)
AS
BEGIN
IF NOT #NUMBER BETWEEN 1 AND 3999 RETURN NULL
DECLARE #ones INT = #NUMBER, #fives INT, #tens INT, #fifties INT, #hundreds INT, #fivehundreds INT, #thousands INT
SET #thousands = #ones / 1000
SET #ones = #ones % 1000
SET #fivehundreds = #ones / 500
SET #ones = #ones % 500
SET #hundreds = #ones / 100
SET #ones = #ones % 100
SET #fifties = #ones / 50
SET #ones = #ones % 50
SET #tens = #ones / 10
SET #ones = #ones % 10
SET #fives = #ones / 5
SET #ones = #ones % 5
RETURN replicate('M', #thousands)
+ iif(#hundreds = 4, iif(#fivehundreds = 1, 'CM', 'CD'), replicate('D', #fivehundreds) + replicate('C', #hundreds))
+ iif(#tens = 4, iif(#fifties = 1, 'XC', 'XL'), replicate('L', #fifties) + replicate('X', #tens))
+ iif(#ones = 4, iif(#fives = 1, 'IX', 'IV'), replicate('V', #fives) + replicate('I', #ones))
END
go
CREATE FUNCTION dbo.uf_RomanToNumberHelper(#roman VARCHAR(128), #len INT)
RETURNS INT
AS
BEGIN
SET #roman = RIGHT(#roman, #len)
DECLARE #NUMBER INT
IF #len = 0 SET #NUMBER = 0
ELSE IF #roman LIKE 'M%' SET #NUMBER = 1000 + dbo.uf_RomanToNumberHelper(#roman, #len - 1)
ELSE IF #roman LIKE 'CM%' SET #NUMBER = 900 + dbo.uf_RomanToNumberHelper(#roman, #len - 2)
ELSE IF #roman LIKE 'D%' SET #NUMBER = 500 + dbo.uf_RomanToNumberHelper(#roman, #len - 1)
ELSE IF #roman LIKE 'CD%' SET #NUMBER = 400 + dbo.uf_RomanToNumberHelper(#roman, #len - 2)
ELSE IF #roman LIKE 'C%' SET #NUMBER = 100 + dbo.uf_RomanToNumberHelper(#roman, #len - 1)
ELSE IF #roman LIKE 'XC%' SET #NUMBER = 90 + dbo.uf_RomanToNumberHelper(#roman, #len - 2)
ELSE IF #roman LIKE 'L%' SET #NUMBER = 50 + dbo.uf_RomanToNumberHelper(#roman, #len - 1)
ELSE IF #roman LIKE 'XL%' SET #NUMBER = 40 + dbo.uf_RomanToNumberHelper(#roman, #len - 2)
ELSE IF #roman LIKE 'X%' SET #NUMBER = 10 + dbo.uf_RomanToNumberHelper(#roman, #len - 1)
ELSE IF #roman LIKE 'IX%' SET #NUMBER = 9 + dbo.uf_RomanToNumberHelper(#roman, #len - 2)
ELSE IF #roman LIKE 'V%' SET #NUMBER = 5 + dbo.uf_RomanToNumberHelper(#roman, #len - 1)
ELSE IF #roman LIKE 'IV%' SET #NUMBER = 4 + dbo.uf_RomanToNumberHelper(#roman, #len - 2)
ELSE IF #roman LIKE 'I%' SET #NUMBER = 1 + dbo.uf_RomanToNumberHelper(#roman, #len - 1)
RETURN #NUMBER
END
go
CREATE FUNCTION dbo.uf_RomanToNumber(#roman VARCHAR(128))
RETURNS INT
AS
BEGIN
DECLARE #len INT = len(#roman)
IF NOT #len BETWEEN 1 AND 30 RETURN NULL
DECLARE #NUMBER INT = dbo.uf_RomanToNumberHelper(#roman, #len)
IF dbo.uf_NumberToRoman(#NUMBER) != #roman RETURN NULL
RETURN #NUMBER
END
go

Related

How to decode BASE64 text (from JSON data) in TSQL with accents intact [duplicate]

I have a column in SQL Server with utf8 SQL_Latin1_General_CP1_CI_AS encoding. How can I convert and save the text in ISO 8859-1 encoding? I would like to do thing in a query on SQL Server. Any tips?
Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito
I have written a function to repair UTF-8 text that is stored in a varchar field.
To check the fixed values you can use it like this:
CREATE TABLE #Table1 (Column1 varchar(max))
INSERT #Table1
VALUES ('Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito')
SELECT *, NewColumn1 = dbo.DecodeUTF8String(Column1)
FROM Table1
WHERE Column1 <> dbo.DecodeUTF8String(Column1)
Output:
Column1
-------------------------------
Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito
NewColumn1
-------------------------------
Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito
The code:
CREATE FUNCTION dbo.DecodeUTF8String (#value varchar(max))
RETURNS nvarchar(max)
AS
BEGIN
-- Transforms a UTF-8 encoded varchar string into Unicode
-- By Anthony Faull 2014-07-31
DECLARE #result nvarchar(max);
-- If ASCII or null there's no work to do
IF (#value IS NULL
OR #value NOT LIKE '%[^ -~]%' COLLATE Latin1_General_BIN
)
RETURN #value;
-- Generate all integers from 1 to the length of string
WITH e0(n) AS (SELECT TOP(POWER(2,POWER(2,0))) NULL FROM (VALUES (NULL),(NULL)) e(n))
, e1(n) AS (SELECT TOP(POWER(2,POWER(2,1))) NULL FROM e0 CROSS JOIN e0 e)
, e2(n) AS (SELECT TOP(POWER(2,POWER(2,2))) NULL FROM e1 CROSS JOIN e1 e)
, e3(n) AS (SELECT TOP(POWER(2,POWER(2,3))) NULL FROM e2 CROSS JOIN e2 e)
, e4(n) AS (SELECT TOP(POWER(2,POWER(2,4))) NULL FROM e3 CROSS JOIN e3 e)
, e5(n) AS (SELECT TOP(POWER(2.,POWER(2,5)-1)-1) NULL FROM e4 CROSS JOIN e4 e)
, numbers(position) AS
(
SELECT TOP(DATALENGTH(#value)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM e5
)
-- UTF-8 Algorithm (http://en.wikipedia.org/wiki/UTF-8)
-- For each octet, count the high-order one bits, and extract the data bits.
, octets AS
(
SELECT position, highorderones, partialcodepoint
FROM numbers a
-- Split UTF8 string into rows of one octet each.
CROSS APPLY (SELECT octet = ASCII(SUBSTRING(#value, position, 1))) b
-- Count the number of leading one bits
CROSS APPLY (SELECT highorderones = 8 - FLOOR(LOG( ~CONVERT(tinyint, octet) * 2 + 1)/LOG(2))) c
CROSS APPLY (SELECT databits = 7 - highorderones) d
CROSS APPLY (SELECT partialcodepoint = octet % POWER(2, databits)) e
)
-- Compute the Unicode codepoint for each sequence of 1 to 4 bytes
, codepoints AS
(
SELECT position, codepoint
FROM
(
-- Get the starting octect for each sequence (i.e. exclude the continuation bytes)
SELECT position, highorderones, partialcodepoint
FROM octets
WHERE highorderones <> 1
) lead
CROSS APPLY (SELECT sequencelength = CASE WHEN highorderones in (1,2,3,4) THEN highorderones ELSE 1 END) b
CROSS APPLY (SELECT endposition = position + sequencelength - 1) c
CROSS APPLY
(
-- Compute the codepoint of a single UTF-8 sequence
SELECT codepoint = SUM(POWER(2, shiftleft) * partialcodepoint)
FROM octets
CROSS APPLY (SELECT shiftleft = 6 * (endposition - position)) b
WHERE position BETWEEN lead.position AND endposition
) d
)
-- Concatenate the codepoints into a Unicode string
SELECT #result = CONVERT(xml,
(
SELECT NCHAR(codepoint)
FROM codepoints
ORDER BY position
FOR XML PATH('')
)).value('.', 'nvarchar(max)');
RETURN #result;
END
GO
Jason Penny has also written an SQL function to convert UTF-8 to Unicode (MIT licence) which worked on a simple example for me:
CREATE FUNCTION dbo.UTF8_TO_NVARCHAR(#in VarChar(MAX))
RETURNS NVarChar(MAX)
AS
BEGIN
DECLARE #out NVarChar(MAX), #i int, #c int, #c2 int, #c3 int, #nc int
SELECT #i = 1, #out = ''
WHILE (#i <= Len(#in))
BEGIN
SET #c = Ascii(SubString(#in, #i, 1))
IF (#c < 128)
BEGIN
SET #nc = #c
SET #i = #i + 1
END
ELSE IF (#c > 191 AND #c < 224)
BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #nc = (((#c & 31) * 64 /* << 6 */) | (#c2 & 63))
SET #i = #i + 2
END
ELSE
BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #c3 = Ascii(SubString(#in, #i + 2, 1))
SET #nc = (((#c & 15) * 4096 /* << 12 */) | ((#c2 & 63) * 64 /* << 6 */) | (#c3 & 63))
SET #i = #i + 3
END
SET #out = #out + NChar(#nc)
END
RETURN #out
END
GO
The ticked answer by Anthony "looks" better to me, but maybe run both if doing conversion and investigate any discrepencies?!
Also we used the very ugly code below to detect BMP page unicode characters that were encoded as UTF-8 and then converted from varchar to nvarchar fields, that can be converted to UCS-16.
LIKE (N'%[' + CONVERT(NVARCHAR,(CHAR(192))) + CONVERT(NVARCHAR,(CHAR(193))) + CONVERT(NVARCHAR,(CHAR(194))) + CONVERT(NVARCHAR,(CHAR(195))) + CONVERT(NVARCHAR,(CHAR(196))) + CONVERT(NVARCHAR,(CHAR(197))) + CONVERT(NVARCHAR,(CHAR(198))) + CONVERT(NVARCHAR,(CHAR(199))) + CONVERT(NVARCHAR,(CHAR(200))) + CONVERT(NVARCHAR,(CHAR(201))) + CONVERT(NVARCHAR,(CHAR(202))) + CONVERT(NVARCHAR,(CHAR(203))) + CONVERT(NVARCHAR,(CHAR(204))) + CONVERT(NVARCHAR,(CHAR(205))) + CONVERT(NVARCHAR,(CHAR(206))) + CONVERT(NVARCHAR,(CHAR(207))) + CONVERT(NVARCHAR,(CHAR(208))) + CONVERT(NVARCHAR,(CHAR(209))) + CONVERT(NVARCHAR,(CHAR(210))) + CONVERT(NVARCHAR,(CHAR(211))) + CONVERT(NVARCHAR,(CHAR(212))) + CONVERT(NVARCHAR,(CHAR(213))) + CONVERT(NVARCHAR,(CHAR(214))) + CONVERT(NVARCHAR,(CHAR(215))) + CONVERT(NVARCHAR,(CHAR(216))) + CONVERT(NVARCHAR,(CHAR(217))) + CONVERT(NVARCHAR,(CHAR(218))) + CONVERT(NVARCHAR,(CHAR(219))) + CONVERT(NVARCHAR,(CHAR(220))) + CONVERT(NVARCHAR,(CHAR(221))) + CONVERT(NVARCHAR,(CHAR(222))) + CONVERT(NVARCHAR,(CHAR(223))) + CONVERT(NVARCHAR,(CHAR(224))) + CONVERT(NVARCHAR,(CHAR(225))) + CONVERT(NVARCHAR,(CHAR(226))) + CONVERT(NVARCHAR,(CHAR(227))) + CONVERT(NVARCHAR,(CHAR(228))) + CONVERT(NVARCHAR,(CHAR(229))) + CONVERT(NVARCHAR,(CHAR(230))) + CONVERT(NVARCHAR,(CHAR(231))) + CONVERT(NVARCHAR,(CHAR(232))) + CONVERT(NVARCHAR,(CHAR(233))) + CONVERT(NVARCHAR,(CHAR(234))) + CONVERT(NVARCHAR,(CHAR(235))) + CONVERT(NVARCHAR,(CHAR(236))) + CONVERT(NVARCHAR,(CHAR(237))) + CONVERT(NVARCHAR,(CHAR(238))) + CONVERT(NVARCHAR,(CHAR(239)))
+ N'][' + CONVERT(NVARCHAR,(CHAR(128))) + CONVERT(NVARCHAR,(CHAR(129))) + CONVERT(NVARCHAR,(CHAR(130))) + CONVERT(NVARCHAR,(CHAR(131))) + CONVERT(NVARCHAR,(CHAR(132))) + CONVERT(NVARCHAR,(CHAR(133))) + CONVERT(NVARCHAR,(CHAR(134))) + CONVERT(NVARCHAR,(CHAR(135))) + CONVERT(NVARCHAR,(CHAR(136))) + CONVERT(NVARCHAR,(CHAR(137))) + CONVERT(NVARCHAR,(CHAR(138))) + CONVERT(NVARCHAR,(CHAR(139))) + CONVERT(NVARCHAR,(CHAR(140))) + CONVERT(NVARCHAR,(CHAR(141))) + CONVERT(NVARCHAR,(CHAR(142))) + CONVERT(NVARCHAR,(CHAR(143))) + CONVERT(NVARCHAR,(CHAR(144))) + CONVERT(NVARCHAR,(CHAR(145))) + CONVERT(NVARCHAR,(CHAR(146))) + CONVERT(NVARCHAR,(CHAR(147))) + CONVERT(NVARCHAR,(CHAR(148))) + CONVERT(NVARCHAR,(CHAR(149))) + CONVERT(NVARCHAR,(CHAR(150))) + CONVERT(NVARCHAR,(CHAR(151))) + CONVERT(NVARCHAR,(CHAR(152))) + CONVERT(NVARCHAR,(CHAR(153))) + CONVERT(NVARCHAR,(CHAR(154))) + CONVERT(NVARCHAR,(CHAR(155))) + CONVERT(NVARCHAR,(CHAR(156))) + CONVERT(NVARCHAR,(CHAR(157))) + CONVERT(NVARCHAR,(CHAR(158))) + CONVERT(NVARCHAR,(CHAR(159))) + CONVERT(NVARCHAR,(CHAR(160))) + CONVERT(NVARCHAR,(CHAR(161))) + CONVERT(NVARCHAR,(CHAR(162))) + CONVERT(NVARCHAR,(CHAR(163))) + CONVERT(NVARCHAR,(CHAR(164))) + CONVERT(NVARCHAR,(CHAR(165))) + CONVERT(NVARCHAR,(CHAR(166))) + CONVERT(NVARCHAR,(CHAR(167))) + CONVERT(NVARCHAR,(CHAR(168))) + CONVERT(NVARCHAR,(CHAR(169))) + CONVERT(NVARCHAR,(CHAR(170))) + CONVERT(NVARCHAR,(CHAR(171))) + CONVERT(NVARCHAR,(CHAR(172))) + CONVERT(NVARCHAR,(CHAR(173))) + CONVERT(NVARCHAR,(CHAR(174))) + CONVERT(NVARCHAR,(CHAR(175))) + CONVERT(NVARCHAR,(CHAR(176))) + CONVERT(NVARCHAR,(CHAR(177))) + CONVERT(NVARCHAR,(CHAR(178))) + CONVERT(NVARCHAR,(CHAR(179))) + CONVERT(NVARCHAR,(CHAR(180))) + CONVERT(NVARCHAR,(CHAR(181))) + CONVERT(NVARCHAR,(CHAR(182))) + CONVERT(NVARCHAR,(CHAR(183))) + CONVERT(NVARCHAR,(CHAR(184))) + CONVERT(NVARCHAR,(CHAR(185))) + CONVERT(NVARCHAR,(CHAR(186))) + CONVERT(NVARCHAR,(CHAR(187))) + CONVERT(NVARCHAR,(CHAR(188))) + CONVERT(NVARCHAR,(CHAR(189))) + CONVERT(NVARCHAR,(CHAR(190))) + CONVERT(NVARCHAR,(CHAR(191)))
+ N']%') COLLATE Latin1_General_BIN
The above:
detects multi-byte sequences encoding U+0080 to U+FFFF (U+0080 to U+07FF is encoded as 110xxxxx 10xxxxxx, U+0800 to U+FFFF is encoded as 1110xxxx 10xxxxxx 10xxxxxx)
i.e. it detects hex byte 0xC0 to 0xEF followed by hex byte 0x80 to 0xBF
ignores ASCII control characters U+0000 to U+001F
ignores characters that are already correctly encoded to unicode >= U+0100 (i.e. not UTF-8)
ignores unicode characters U+0080 to U+00FF if they don't appear to be part of a UTF-8 sequence e.g. "coöperatief".
doesn't use LIKE "%[X-Y]" for X=0x80 to Y=0xBF because of potential collation issues
uses CONVERT(VARCHAR,CHAR(X)) instead of NCHAR because we had problems with NCHAR getting converted to the wrong value (for some values).
ignores UTF characters greater than U+FFFF (4 to 6 byte sequences which have a first byte of hex 0xF0 to 0xFD)
I made a solution that also handles 4 byte sequences (like emojis) by combining the answer from #robocat, some more cases with the logic taken from https://github.com/benkasminbullock/unicode-c/blob/master/unicode.c, and a solution for the problem of encoding extended unicode characters from https://dba.stackexchange.com/questions/139551/how-do-i-set-a-sql-server-unicode-nvarchar-string-to-an-emoji-or-supplementary. It's not fast or pretty, but it's working for me anyway. This particular solution includes Unicode replacement characters wherever it finds unknown bytes. It may be better just to throw an exception in these cases, or leave the bytes as they were, as future encoding could be off, but I preferred this for my use case.
-- Started with https://stackoverflow.com/questions/28168055/convert-text-value-in-sql-server-from-utf8-to-iso-8859-1
-- Modified following source in https://github.com/benkasminbullock/unicode-c/blob/master/unicode.c
-- Made characters > 65535 work using https://dba.stackexchange.com/questions/139551/how-do-i-set-a-sql-server-unicode-nvarchar-string-to-an-emoji-or-supplementary
CREATE FUNCTION dbo.UTF8_TO_NVARCHAR(#in VarChar(MAX)) RETURNS NVarChar(MAX) AS
BEGIN
DECLARE #out NVarChar(MAX), #thisOut NVARCHAR(MAX), #i int, #c int, #c2 int, #c3 int, #c4 int
SELECT #i = 1, #out = ''
WHILE (#i <= Len(#in)) BEGIN
SET #c = Ascii(SubString(#in, #i, 1))
IF #c <= 0x7F BEGIN
SET #thisOut = NCHAR(#c)
SET #i = #i + 1
END
ELSE IF #c BETWEEN 0xC2 AND 0xDF BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
IF #c2 < 0x80 OR #c2 > 0xBF BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
ELSE BEGIN
SET #thisOut = NCHAR(((#c & 31) * 64 /* << 6 */) | (#c2 & 63))
SET #i = #i + 2
END
END
ELSE IF #c BETWEEN 0xE0 AND 0xEF BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #c3 = Ascii(SubString(#in, #i + 2, 1))
IF #c2 < 0x80 OR #c2 > 0xBF OR #c3 < 0x80 OR (#c = 0xE0 AND #c2 < 0xA0) BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
ELSE BEGIN
SET #thisOut = NCHAR(((#c & 15) * 4096 /* << 12 */) | ((#c2 & 63) * 64 /* << 6 */) | (#c3 & 63))
SET #i = #i + 3
END
END
ELSE IF #c BETWEEN 0xF0 AND 0xF4 BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #c3 = Ascii(SubString(#in, #i + 2, 1))
SET #c4 = Ascii(SubString(#in, #i + 3, 1))
IF #c2 < 0x80 OR #c2 >= 0xC0 OR #c3 < 0x80 OR #c3 >= 0xC0 OR #c4 < 0x80 OR #c4 >= 0xC0 OR (#c = 0xF0 AND #c2 < 0x90) BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
ELSE BEGIN
DECLARE #nc INT = (((#c & 0x07) * 262144 /* << 18 */) | ((#c2 & 0x3F) * 4096 /* << 12 */) | ((#c3 & 0x3F) * 64) | (#c4 & 0x3F))
DECLARE #HighSurrogateInt INT = 55232 + (#nc / 1024), #LowSurrogateInt INT = 56320 + (#nc % 1024)
SET #thisOut = NCHAR(#HighSurrogateInt) + NCHAR(#LowSurrogateInt)
SET #i = #i + 4
END
END
ELSE BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
SET #out = #out + #thisOut
END
RETURN #out
END
GO
i add a little modification to use new string aggregation function string_agg, from sql server 2017 and 2019
SELECT #result=STRING_AGG(NCHAR([codepoint]),'') WITHIN GROUP (ORDER BY position ASC)
FROM codepoints
change de #result parts to this one. The XML still work in old fashion way.
in 2019, string_agg works extreme faster than xml version (obvious... string_agg now is native, and is not fair compare)
Here's my version written as an inline table-valued function (TVF) for SQL Server 2017. It is limited to 4000 byte input strings as that was more than enough for my needs. Limiting the input size and writing as a TVF makes this version significantly faster than the scaler valued functions posted so far. It also handles four-byte UTF-8 sequences (such as those created by emoji), which cannot be represented in UCS-2 strings, by outputting a replacement character in their place.
CREATE OR ALTER FUNCTION [dbo].[fnUTF8Decode](#UTF8 VARCHAR(4001)) RETURNS TABLE AS RETURN
/* Converts a UTF-8 encoded VARCHAR to NVARCHAR (UCS-2). Based on UTF-8 documentation on Wikipedia and the
code/discussion at https://stackoverflow.com/a/31064459/1979220.
One can quickly detect strings that need conversion using the following expression:
<FIELD> LIKE CONCAT('%[', CHAR(192), '-', CHAR(255), ']%') COLLATE Latin1_General_BIN.
Be aware, however, that this may return true for strings that this function has already converted to UCS-2.
See robocat's answer on the above referenced Stack Overflow thread for a slower but more robust expression.
Notes/Limitations
1) Written as a inline table-valued function for optimized performance.
2) Only tested on a database with SQL_Latin1_General_CP1_CI_AS collation. More specifically, this was
not designed to output Supplementary Characters and converts all such UTF-8 sequences to �.
3) Empty input strings, '', and strings with nothing but invalid UTF-8 chars are returned as NULL.
4) Assumes input is UTF-8 compliant. For example, extended ASCII characters such as en dash CHAR(150)
are not allowed unless part of a multi-byte sequence and will be skipped otherwise. In other words:
SELECT * FROM dbo.fnUTF8Decode(CHAR(150)) -> NULL
5) Input should be limited to 4000 characters to ensure that output will fit in NVARCHAR(4000), which is
what STRING_AGG outputs when fed a sequence of NVARCHAR(1) characters generated by NCHAR. However,
T-SQL silently truncates overlong parameters so we've declared our input as VARCHAR(4001) to allow
STRING_AGG to generate an error on overlong input. If we didn't do this, callers would never be
notified about truncation.
6) If we need to process more than 4000 chars in the future, we'll need to change input to VARCHAR(MAX) and
CAST the CASE WHEN expression to NVARCHAR(MAX) to force STRING_AGG to output NVARCHAR(MAX). Note that
this change will significantly degrade performance, which is why we didn't do it in the first place.
7) Due to use of STRING_AGG, this is only compatible with SQL 2017. It will probably work fine on 2019
but that version has native UTF-8 support so you're probably better off using that. For earlier versions,
replace STRING_AGG with a CLR equivalent (ms-sql-server-group-concat-sqlclr) or FOR XML PATH(''), TYPE...
*/
SELECT STRING_AGG (
CASE
WHEN A1 & 0xF0 = 0xF0 THEN --Four byte sequences (like emoji) can't be represented in UCS-2
NCHAR(0xFFFD) --Output U+FFFD (Replacement Character) instead
WHEN A1 & 0xE0 = 0xE0 THEN --Three byte sequence; get/combine relevant bits from A1-A3
NCHAR((A1 & 0x0F) * 4096 | (A2 & 0x3F) * 64 | (A3 & 0x3F))
WHEN A1 & 0xC0 = 0xC0 THEN --Two byte sequence; get/combine relevant bits from A1-A2
NCHAR((A1 & 0x3F) * 64 | (A2 & 0x3F))
ELSE NCHAR(A1) --Regular ASCII character; output as is
END
, '') UCS2
FROM dbo.fnNumbers(ISNULL(DATALENGTH(#UTF8), 0))
CROSS APPLY (SELECT ASCII(SUBSTRING(#UTF8, I, 1)) A1, ASCII(SUBSTRING(#UTF8, I + 1, 1)) A2, ASCII(SUBSTRING(#UTF8, I + 2, 1)) A3) A
WHERE A1 <= 127 OR A1 >= 192 --Output only ASCII chars and one char for each multi-byte sequence
GO
Note that the above requires a "Numbers" table or generator function. Here's the function I use:
CREATE OR ALTER FUNCTION [dbo].[fnNumbers](#MaxNumber BIGINT) RETURNS TABLE AS RETURN
/* Generates a table of numbers up to the specified #MaxNumber, limited to 4,294,967,296. Useful for special case
situations and algorithms. Copied from https://www.itprotoday.com/sql-server/virtual-auxiliary-table-numbers
with minor formatting and name changes.
*/
WITH L0 AS (
SELECT 1 I UNION ALL SELECT 1 --Generates 2 rows
), L1 AS (
SELECT 1 I FROM L0 CROSS JOIN L0 L -- 4 rows
), L2 AS (
SELECT 1 I FROM L1 CROSS JOIN L1 L -- 16 rows
), L3 AS (
SELECT 1 I FROM L2 CROSS JOIN L2 L -- 256 rows
), L4 AS (
SELECT 1 I FROM L3 CROSS JOIN L3 L -- 65,536 rows
), L5 AS (
SELECT 1 I FROM L4 CROSS JOIN L4 L -- 4,294,967,296 rows
), Numbers AS (
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) I FROM L5
)
SELECT TOP (#MaxNumber) I FROM Numbers ORDER BY I
GO
I just succeeded by creating a new field as varchar(255) and setting the new field to the old field which was nvarchar(255). This produced the 'Americanized' version of the international places.
Update WorldCities
Set admin_correct = admin_name
varchar(255) nvarchar(255)
I found the query I need to do, just not the encoding yet.
ALTER TABLE dbo.MyTable ALTER COLUMN CharCol
varchar(10)COLLATE Latin1_General_CI_AS NOT NULL;

How do I loop through a stored procedure within a sql statement

I know this sounds like an old question, but I haven't been able to solve my problem even after reading endless answers, so I'll try to be specific.
I have a table with a column of text, which sometimes includes HTML tags. I need to remove MOST of the HTML but leave some. I've written the following code which will do that:
DECLARE #Start INT = 1;
DECLARE #End INT = 1;
DECLARE #Length INT = 0;
DECLARE #Keep INT = 0;
DECLARE #ReplaceChar VARCHAR(10) = '';
DECLARE #Offset INT = 0;
RETURN;
WHILE #Start > 0 AND #End > 0
BEGIN
SET #ReplaceChar = (SELECT CASE WHEN SUBSTRING(#HTMLText,#Start + 1, 7) IN ('/strike')
THEN ('</strike>')
WHEN SUBSTRING(#HTMLText,#Start + 1, 6) IN ('strike')
THEN ('<strike>')
WHEN SUBSTRING(#HTMLText,#Start + 1, 4) IN ('br /')
THEN ('<br />')
WHEN SUBSTRING(#HTMLText,#Start + 1, 3) IN ('/th', '/tr','/td','th ')
THEN ('<' + SUBSTRING(#HTMLText,#Start + 1, 3) + '>')
WHEN SUBSTRING(#HTMLText,#Start + 1, 2) IN ('th', 'tr', 'td')
THEN ('<' + SUBSTRING(#HTMLText,#Start + 1, 2) + '>')
WHEN SUBSTRING(#HTMLText,#Start + 1, 2) IN ('/p', '/i', '/b')
THEN ('<' + SUBSTRING(#HTMLText,#Start + 1, 2) + '>')
WHEN SUBSTRING(#HTMLText,#Start + 1, 1) IN ('p', 'i', 'b')
THEN ('<' + SUBSTRING(#HTMLText,#Start + 1, 1) + '>')
ELSE ''
END);
SET #Keep = (SELECT CASE WHEN SUBSTRING(#HTMLText,#Start + 1, 7) IN ('/strike') THEN 1
WHEN SUBSTRING(#HTMLText,#Start + 1, 6) IN ('strike') THEN 1
WHEN SUBSTRING(#HTMLText,#Start + 1, 4) IN ('br /') THEN 1
WHEN SUBSTRING(#HTMLText,#Start + 1, 3) IN ('/th', '/tr', '/td') THEN 1
WHEN SUBSTRING(#HTMLText,#Start + 1, 2) IN ('th', 'tr', 'td') THEN 1
WHEN SUBSTRING(#HTMLText,#Start + 1, 2) IN ('/p', '/i', '/b') THEN 1
WHEN SUBSTRING(#HTMLText,#Start + 1, 1) IN ('p', 'i', 'b') THEN 1
ELSE 0
END);
SET #HTMLText = RTRIM((LTRIM(STUFF(#HTMLText,#Start, #Length,#ReplaceChar))));
SET #Start = CHARINDEX('<', #HTMLText,#Start + #Offset);
SET #End = CHARINDEX('>', #HTMLText,CHARINDEX('<', #HTMLText,#Start + #Offset));
SET #Length = (#End - #Start) + 1;
SET #Offset = #Start + #Keep;
END;
RETURN #HTMLText;
This needs to be applied to the Details column in the following code, after the Replace functions have completed.
DECLARE #Region NVARCHAR(12) = '11'
DECLARE #Location NVARCHAR (1000) = '1932'
DECLARE #IPM NVARCHAR (500) = '1594,1611,1934' -- for 1932 --'8055,15591'--for 1941
DECLARE #NoteFromDate DATETIME = '20150101'
DECLARE #NoteToDate DATETIME= '20160701'
SELECT r.RegionName
, l.LocationName
, CASE WHEN o.CurrentLocationId <> l.LocationId
THEN '*'
ELSE ''
END AS NotCurrentLocation
, e.DisplayName AS IPM
, ec.DisplayName AS Counselor
, oni.OffenderId
, an.LastName + ', ' + an.FirstName + COALESCE(' ' + an.MiddleName,
'') AS OffenderName
, oni.NoteDate
,REPLACE(REPLACE(REPLACE(REPLACE(
ono.Details,'''',''),' ',' '),'&',' & '),'rsquo;','-')
FROM ref.Employee AS e
INNER JOIN ref.Location AS l
ON e.LocationId = l.LocationId
INNER JOIN ref.Region AS r
ON l.RegionId = r.RegionId
INNER JOIN ind.OffenderNoteInfo AS oni
ON e.EmployeeId = oni.StaffId
INNER JOIN ind.Offender AS o
ON oni.OffenderId = o.OffenderId
INNER JOIN ind.OffenderNote AS ono
ON oni.OffenderNoteInfoId = ono.OffenderNoteInfoId
INNER JOIN ind.OffenderNoteInfo_ContactMode AS onicm
ON oni.OffenderNoteInfoId = onicm.OffenderNoteInfoId
INNER JOIN ind.AliasName AS an
ON oni.OffenderId = an.OffenderId AND an.AliasNameTypeId = 0 --Default Name
LEFT JOIN ind.OffenderCurrentFactPart AS ocfp
ON o.OffenderId = ocfp.OffenderId
LEFT JOIN hsn.CounselorAssignment AS ca
ON ocfp.PriCounselorAssignmentId = ca.CounselorAssignmentId
LEFT JOIN ref.Employee AS ec
ON ca.EmployeeId = ec.EmployeeId
WHERE e.LocationId IN (
SELECT Value
FROM vnfa.udf_FnSplit(#Location, ',')) AND e.EmployeeTypeId = 106 --Treatment Program Supervisor
AND oni.NoteTypeId = 11 --Facility Notes
AND onicm.ContactModeId = 229 --Institution Fidelity Review
AND (oni.NoteDate >= #NoteFromDate AND oni.NoteDate <= #NoteToDate) AND e.EmployeeId IN (
SELECT Value
FROM vnfa.udf_FnSplit(#IPM, ','));
How do I get code section 1 to run within code section two?
Input for the Details column might look like:
<p style="margin: 0in 0in 0pt;"><span style="font-family: Calibri;">All areas were reviewed </span></p>
Output should be:
<p>All areas were reviewed</p>
I'm just not seeing a way to combine these two for each row. Thanks for any suggestions.
I would do as others suggest and write this code in another language. You can do it with tsql however. The first code can be a scalar function that can then be applied to the details column in your query.

Sum until value reached zero

I have two queries.
The result of the first one is the OnHand quantity of the part in the warehouse:
PartNum OnHandQty IUM
100009 19430.00 KG
The result of the second query are transactions related to the receipts of the goods to the warehouse:
TranDate PartNum TranQty UM Dayss
2014-09-01 100009 10720.000 KG 2
2014-09-01 100009 1340.000 KG 2
2014-08-11 100009 8710.000 KG 23
2014-08-11 100009 3350.000 KG 23
2014-06-30 100009 9380.000 KG 65
Now I need to calculate OnHandQy - TranQty until it hits zero, e.g.
19430 - 10720 = 8710 --not enough
8710 - 1340 = 7370 --not enough
7370 - 8710 = -1340 --enough
As a result I need to receive a table like below:
PartNum OnHandQty IUM [0-10 Days] [11-20 Days] [over 21 Days]
100009 19430.00 KG 12060 null 8710
Any ideas how to get this result?
One idea is to use the following query.
WITH(TranDate,PartNum,TranQty,UM,Dayss) trans
AS
(
-- Your second query
)
SELECT F.PartNum
,F.OnHandQty
,F.IUM
,SUM(t0to10.TransQty) AS [0-10 Days]
,SUM(t11to20.TransQty) AS [11-20 Days]
,SUM(tover20.TransQty) AS [over 21 Days]
FROM (Your first query) AS F
JOIN trans AS t0to10 ON F.PartNum = t0to10.PartNum
AND t0to10.Dayss BETWEEN 0 AND 10
JOIN trans AS t11to20 ON F.PartNum = t11to20.PartNum
AND t11to20.Dayss BETWEEN 11 AND 20
JOIN trans AS tover20 ON F.PartNum = tover20.PartNum
AND tover20.Dayss >20
GROUP BY F.PartNum,F.OnHandQty,F.IUM
Declared the OnHandQty and using cursor in a while loop will help to sort this out.
CREATE TABLE #OnHand(
PartNum NVARCHAR(100)
,OnHandQty DECIMAL
,IUM NVARCHAR(5)
)
INSERT INTO #OnHand
(PartNum, OnHandQty, IUM)
VALUES
('100009', 19430.00, 'KG')
CREATE TABLE #Trans(
TranDate DATETIME
,PartNum NVARCHAR(100)
,TranQty DECIMAL
,IUM NVARCHAR(5))
INSERT INTO #Trans
(TranDate, PartNum, TranQty, IUM)
VALUES
('2014-09-01', '100009', 10720.000, 'KG')
,('2014-09-01', '100009', 1340.000, 'KG')
,('2014-08-11', '100009', 8710.000, 'KG')
,('2014-08-11', '100009', 3350.000, 'KG')
,('2014-06-30', '100009', 9380.000, 'KG')
DECLARE #OnHand_Running DECIMAL
,#OnHandQty DECIMAL
,#TranQty DECIMAL
,#TranDate DATETIME
,#PartNum NVARCHAR(100)
,#First10Days INT = 0
,#Second10Days INT = 0
,#Third10Days INT = 0
SET #PartNum = '100009';
Set #OnHandQty = (Select OnHandQty
FROM #OnHand
WHERE PartNum = #PartNum)
Set #OnHand_Running = #OnHandQty
DECLARE tran_cursor CURSOR
FOR SELECT T.TranQty, T.TranDate
FROM #Trans T
ORDER BY T.TranDate DESC
OPEN tran_cursor
FETCH NEXT FROM tran_cursor
INTO #TranQty, #TranDate
WHILE (##FETCH_STATUS = 0) AND (#OnHandQty > 0)
BEGIN
IF (#OnHandQty - #TranQty) < 0
BEGIN
PRINT CAST(#OnHandQty AS NVARCHAR(100)) + '-' + CAST(#TranQty AS NVARCHAR(100)) + '=' + CAST(#OnHandQty - #TranQty AS NVARCHAR(100)) + ' --Enough'
END
ELSE
BEGIN
PRINT CAST(#OnHandQty AS NVARCHAR(100)) + '-' + CAST(#TranQty AS NVARCHAR(100)) + '=' + CAST(#OnHandQty - #TranQty AS NVARCHAR(100)) + ' --Not Enough'
END
IF (DATEDIFF(DAY, #TranDate, CURRENT_TIMESTAMP) < 10)
SET #First10Days = #First10Days + #TranQty
IF (DATEDIFF(DAY, #TranDate, CURRENT_TIMESTAMP) BETWEEN 10 AND 20)
SET #Second10Days = #Second10Days + #TranQty
IF (DATEDIFF(DAY, #TranDate, CURRENT_TIMESTAMP) > 20)
SET #Third10Days = #Third10Days + #TranQty
SET #OnHandQty = #OnHandQty - #TranQty
FETCH NEXT FROM tran_cursor
INTO #TranQty, #TranDate
END
SELECT #PartNum, #OnHand_Running, 'KG', #First10Days '[0-10] Days', #Second10Days '[11-20] Days', #Third10Days '[over 21 days]'
CLOSE tran_cursor;
DEALLOCATE tran_cursor;
RETURN

mysql-function to count days between 2 dates excluding weekends

I've searched through many examples , good ones I got :
Count days between two dates, excluding weekends (MySQL only)
How to count date difference excluding weekend and holidays in MySQL
Calculate diffference between 2 dates in SQL, excluding weekend days
but didn't get most promising solution , so that i can use in my mysql-function for quering lakhs of rows.
This one was very new concept , but didn't worked for inputs like #start_date = '2013-08-03' , #end_date = '2013-08-21' Expected ans : 13 , its giving only 12,
SELECT 5 * (DATEDIFF(#end_date, #start_date) DIV 7) + MID('0123444401233334012222340111123400012345001234550', 7 * WEEKDAY(#start_date) + WEEKDAY(#end_date) + 1, 1);
So i'did tried to make it by myself -
Concept :
Input : 1. period_from_date - from date
2. period_to_date - to date
3. days_to_exclude - mapping : S M T W TH F Sat => 2^0 + 2^6
(sat and sun to exclude) ^ ^ ^ ^ ^ ^ ^
0 1 2 3 4 5 6
DELIMITER $$
USE `db_name`$$
DROP FUNCTION IF EXISTS `FUNC_CALC_TOTAL_WEEKDAYS`$$
CREATE DEFINER=`name`#`%` FUNCTION `FUNC_CALC_TOTAL_WEEKDAYS`( period_from_date DATE, period_to_date DATE, days_to_exclude INT ) RETURNS INT(11)
BEGIN
DECLARE period_total_num_days INT DEFAULT 0;
DECLARE period_total_working_days INT DEFAULT 0;
DECLARE period_extra_days INT DEFAULT 0;
DECLARE period_complete_weeks INT DEFAULT 0;
DECLARE extra_days_start_date DATE DEFAULT '0000-00-00';
DECLARE num_days_to_exclude INT DEFAULT 0;
DECLARE start_counter_frm INT DEFAULT 0;
DECLARE end_counter_to INT DEFAULT 6;
DECLARE temp_var INT DEFAULT 0;
# if no day to exclude return date-diff only
IF days_to_exclude = 0 THEN
RETURN DATEDIFF( period_to_date, period_from_date ) + 1 ;
END IF;
# get total no of days to exclude
WHILE start_counter_frm <= end_counter_to DO
SET temp_var = POW(2,start_counter_frm) ;
IF (temp_var & days_to_exclude) = temp_var THEN
SET num_days_to_exclude = num_days_to_exclude + 1;
END IF;
SET start_counter_frm = start_counter_frm + 1;
END WHILE;
# Get period days count
SET period_total_num_days = DATEDIFF( period_to_date, period_from_date ) + 1 ;
SET period_complete_weeks = FLOOR( period_total_num_days /7 );
SET period_extra_days = period_total_num_days - ( period_complete_weeks * 7 );
SET period_total_working_days = period_complete_weeks * (7 - num_days_to_exclude);
SET extra_days_start_date = DATE_SUB(period_to_date,INTERVAL period_extra_days DAY);
# get total working days from the left days
WHILE period_extra_days > 0 DO
SET temp_var = DAYOFWEEK(period_to_date) -1;
IF POW(2,temp_var) & days_to_exclude != POW(2,temp_var) THEN
SET period_total_working_days = period_total_working_days +1;
END IF;
SET period_to_date = DATE_SUB(period_to_date,INTERVAL 1 DAY);
SET period_extra_days = period_extra_days -1;
END WHILE;
RETURN period_total_working_days;
END$$
DELIMITER ;
Please let me know the holes where this would fail.Open to any suggestions and comments.
UPDATED: If you just need a number of weekdays between two dates you can get it like this
CREATE FUNCTION TOTAL_WEEKDAYS(date1 DATE, date2 DATE)
RETURNS INT
RETURN ABS(DATEDIFF(date2, date1)) + 1
- ABS(DATEDIFF(ADDDATE(date2, INTERVAL 1 - DAYOFWEEK(date2) DAY),
ADDDATE(date1, INTERVAL 1 - DAYOFWEEK(date1) DAY))) / 7 * 2
- (DAYOFWEEK(IF(date1 < date2, date1, date2)) = 1)
- (DAYOFWEEK(IF(date1 > date2, date1, date2)) = 7);
Note: The function will still work if you switch start date1 and end date2 dates.
Sample usage:
SELECT TOTAL_WEEKDAYS('2013-08-03', '2013-08-21') weekdays1,
TOTAL_WEEKDAYS('2013-08-21', '2013-08-03') weekdays2;
Output:
| WEEKDAYS1 | WEEKDAYS2 |
-------------------------
| 13 | 13 |
Here is DBFiddle demo
This query will work fine, all the queries above are not working well. Try this :
SELECT ((DATEDIFF(date2, date1)) -
((WEEK(date2) - WEEK(date1)) * 2) -
(case when weekday(date2) = 6 then 1 else 0 end) -
(case when weekday(date1) = 5 then 1 else 0 end)) as DifD
Test it like this :
SELECT ((DATEDIFF('2014-10-25', '2014-10-15')) -
((WEEK('2014-10-25') - WEEK('2014-10-15')) * 2) -
(case when weekday('2014-10-25') = 6 then 1 else 0 end) -
(case when weekday('2014-10-15') = 5 then 1 else 0 end)) as DifD
The result :
DifD
8
I use this. Means there are no functions so can be used in views:
select
datediff(#dateto, #datefrom) +
datediff(#datefrom,
date_add(#datefrom, INTERVAL
floor(datediff(#dateto, #datefrom) / 7) day)) * 2
- case
when weekday(#dateto) = 6 then 2
when weekday(#dateto) = 5 then 1
when weekday(#dateto) < weekday(#datefrom) then 2
else 0
end;
Had a similar issue, I used PHP to remove the weekends, need to know start day and number of days:
EG SQL:
SELECT DAYOFWEEK(`date1`) AS `startday`, TIMESTAMPDIFF(DAY, `date1`, `date2`) AS `interval` FROM `table`
Then run the result through a PHP function:
function noweekends($startday, $interval) {
//Remove weekends from an interval
$wecount = 0; $tmp = $interval;
while($interval/7 > 1) { $interval-=7; $wecount++; }
if($interval+$startday > 5) $wecount++;
$interval = $tmp-($wecount*2);
return $interval;
}
To exclude only Sunday:
CREATE FUNCTION TOTAL_WEEKDAYS(date1 DATE, date2 DATE)
RETURNS INT
RETURN ABS(DATEDIFF(date2, date1)) + 1
- ABS(DATEDIFF(ADDDATE(date2, INTERVAL 1 - DAYOFWEEK(date2) DAY),
ADDDATE(date1, INTERVAL 1 - DAYOFWEEK(date1) DAY))) / 7
- (DAYOFWEEK(IF(date1 < date2, date1, date2)) = 1);
You can also create triggers to automatically calculate it in another column, and you can specify legal holidays in another table:
CREATE OR REPLACE TRIGGER `vacation_before_insert` BEFORE INSERT ON `vacation` FOR EACH ROW
BEGIN
SET #start_date = NEW.Start_date;
SET #end_date = NEW.End_date;
SET #numofholydays = (IFNULL((SELECT SUM(IF(`Date` BETWEEN NEW.Start_date AND NEW.End_date, 1, 0)) as numofdays FROM free_legal_days),0));
SET #totaldays = DATEDIFF(#end_date , #start_date) + 1;
SET #saturdays = WEEK(DATE_ADD(#end_date, INTERVAL 1 DAY))-WEEK(#start_date);
SET #sundays = WEEK(#end_date) - WEEK(#start_date);
SET NEW.Number_of_days = #totaldays-#saturdays-#sundays-#numofholydays;
END;
CREATE OR REPLACE TRIGGER `vacation_before_update` BEFORE UPDATE ON `vacation` FOR EACH ROW
BEGIN
SET #start_date = NEW.Start_date;
SET #end_date = NEW.End_date;
SET #numofholydays = (IFNULL((SELECT SUM(IF(`Date` BETWEEN NEW.Start_date AND NEW.End_date, 1, 0)) as numofdays FROM free_legal_days),0));
SET #totaldays = DATEDIFF(#end_date , #start_date) + 1;
SET #saturdays = WEEK(DATE_ADD(#end_date, INTERVAL 1 DAY))-WEEK(#start_date);
SET #sundays = WEEK(#end_date) - WEEK(#start_date);
SET NEW.Number_of_days = #totaldays-#saturdays-#sundays-#numofholydays;
END;

How to round a SQL time variable?

I have declared a time variable with value 23:59:59. So, I need to round it to 24:00. Have you any idea?
declare #t1 time = '23:59:59'
This is necessary only in the select statement. I know that time cannot be inserted as 24:00.
http://msdn.microsoft.com/en-us/library/bb677243(v=sql.105).aspx
How about:
--setup
declare #t1 time = '23:59:59'
--declare #t1 time = '00:59:59'
--declare #t1 time = '00:59:29'
--declare #t1 time = '00:00:00'
--solution for rounding to nearest hour
declare #temp1 int = datepart(minute, #t1) / 30
set #temp1 = (DATEPART(hour, #t1) % 24) + #temp1
select #t1, cast(#temp1 as nvarchar(2)) + ':00' --not too worried about rpading hours since 1:00 makes sense
--solution for rounding to nearest minute
declare #ss int = datepart(second, #t1)/30
, #mm int = datepart(minute, #t1)
, #hh int = datepart(hour, #t1)
, #oo nchar(2) = N'00' --used for padding
set #hh = (#hh + (#mm / 30) * #ss) % 24
set #mm = (#mm + #ss) % 60
select #t1
, substring(#oo + cast(#hh as nvarchar(2)), case when #hh > 9 then 3 when #hh = 0 then 1 else 2 end, 2) --hours rpaded
+ N':'
+ substring(#oo + cast(#mm as nvarchar(2)), case when #mm > 9 then 3 when #mm = 0 then 1 else 2 end, 2) --minutes rpaded