Issue With REPLICATE and Strings in SQL Server 2008 - sql-server

One of the functions of our app is that it prints out "sales tapes" that help the tellers close each night. These tapes print on a 40-character, fixed-width heat-paper printer. At the moment these use deprecated code to load the data from our sales tables instead of the reporting "cube" tables. I'm rewriting them to use the cubes.
I'm running into an issue formatting the text in SQL Server 2008. I'm using the REPLICATE function to calculate out each side of the columns, per line. For some reason some of the lines just randomly have an extra character and are 41 characters in width. Needless to say, that prevents amounts from properly appearing. The two columns are 27 characters and 13 characters
Here is an example. Below is are the pieces. From left to right: left spaces, length of left column text, right spaces, length of right column text. | 40 shows that the total of everything is 40 characters
20 7 7 6 | 40
18 9 3 10 | 40
7 20 13 0 | 40
In this case, the left text is 7 characters, followed by 20 spaces, followed by 7 spaces, followed by 6 characters, all of which would be 40 total characters. What it should read is this (account masked for safety):
STEWART $57.70
AT&T (DP) Fee: $1.50
Acct: xxxxxxxxxxxxxx
However, what it actually reads is:
STEWART $57.7
AT&T (DP) Fee: $1.50
Acct: xxxxxxxxxxxxxx
I can't figure out why it is including too many spaces. If you compare, you can see that $57.70 is 6 characters, as calculated in the first line. Yet it appears as 5 because it is truncated by the 20 + 7. Some how 20 (left spaces) + 7 (left text) + 7 (right spaces) + 6 (right text) is equaling 41!! Below is my code in the UDF:
DECLARE #ReturnValue NVARCHAR(40) = '';
DECLARE #LeftSpaces INT = #LeftSideWidth;
DECLARE #RightSpaces INT = (#PageWidth - #LeftSideWidth);
--remove header text space
SET #LeftSpaces = #LeftSpaces - LEN(#LeftText);
SET #RightSpaces = #RightSpaces - LEN(#RightText);
SET #ReturnValue = #LeftText; --add our left column
SET #ReturnValue = #ReturnValue + REPLICATE(' ', #LeftSpaces); --add our left spaces
SET #ReturnValue = #ReturnValue + REPLICATE(' ', #RightSpaces); --add our right spaces
SET #ReturnValue = #ReturnValue + #RightText; --finally, add our right text;
RETURN #ReturnValue;
The UDF is pretty simple. First, I set the spaces to equal the full length of both columns. Then I reduce the count of spaces by the length of the text to appear in the column on this line. Then I add the left text, left spaces, right spaces, and finally the right-aligned text together and return it. For most rows it works perfect. For random rows (so far those with 6, 7, and 15 length on left text), I get what appeared above. The UDF was written to be more succinct originally but I finally broke it out logically into steps when I couldn't figure out what was wrong.
Anyone have an idea? Where is my math wrong?

try using the LEFT and RIGHT functions along with REPLICATE, like so:
DECLARE #ReturnValue NVARCHAR(40) = '';
DECLARE #LeftSpaces INT = #LeftSideWidth;
DECLARE #RightSpaces INT = (#PageWidth - #LeftSideWidth);
--remove header text space
SET #LeftSpaces = #LeftSpaces - LEN(#LeftText);
SET #RightSpaces = #RightSpaces - LEN(#RightText);
SET #ReturnValue = LEFT(#LeftText + REPLICATE(' ', #LeftSpaces), #LeftSideWidth); --add our left column
SET #ReturnValue = #ReturnValue + RIGHT(REPLICATE(' ', #RightSpaces) + #RightText, (#PageWidth - #LeftSideWidth)); --finally, add our right text;
RETURN #ReturnValue;

Related

Is there a way in SQL server to interpret the underlying varchar(4) bits as an INT?

I have data harvested from a binary file that has been encoded as a SQL column with type varchar(4). This is not changeable. The 4 bytes used to create this varchar need to be interpreted sometimes as an int value (big endian). It would be nice if we could do this entirely inside SQL.
Printing the values in this varchar(4) column is not helpful as most of the bytes get interpreted as unprintable control characters.
I can't figure out how CAST or CONVERT can help since they seem to be tailored to converting a varchar like "0054" to int 54. Instead, I need the underlying bits to be interpreted as an int (big endian)--not the varchar characters as an int.
For example, one record prints this column as no visible characters, but STRING_ESCAPE(#value,'json')
will display
\u0000\u0000\u0000\u0007
This needs to be interpreted somehow to be the int 7
Here's a few more examples of what STRING_ESCAPE returns and what the int value should be:
\u0000\u0000\u0000\b ==> 8
\u0000\u0000\u0000\t ==> 9
\u0000\u0000\u0000\n ==> 10
\u0000\u0000\u0000\u000b ==> 11
\u0000\u0000\u0000\f ==> 12
\u0000\u0000\u0000\r ==> 13
\u0000\u0000\u0000\u000e ==> 14
\u0000\u0000\u0000\u000f ==> 15
\u0000\u0000\u0000\u0010 ==> 16
Thanks for your brain!
So, here is a table of sample data. The first row represents your main example. But you don't have any examples where any one of the first 3 characters is not character 0. So I threw in another row where this is the case.
declare #values table (value char(4))
insert #values values
(char(0) + char(0) + char(0) + char(7)),
(char(13) + char(9) + char(14) + char(8));
In the query below, I isolate each character using substring. Then I call ascii to retrieve the character code. What is not clear, however, is how you would take those integer values and combine them. I give 3 possibilities. 'Option1' concatenates them. 'Option2' sums them together. 'Option3' concatenates them like option1, but pads them first so that there is a leading '0' if it is only one digit long.
select escapedVal = string_escape(value,'json'),
ap.*,
option1 = convert(int,concat(pos1, pos2, pos3, pos4)),
option2 = pos1 + pos2 + pos3 + pos4,
option3 = convert(int,
right('00' + convert(varchar(2),pos1),2) +
right('00' + convert(varchar(2),pos2),2) +
right('00' + convert(varchar(2),pos3),2) +
right('00' + convert(varchar(2),pos4),2)
)
from #values v
cross apply (select
pos1 = ascii(substring(value,1,1)),
pos2 = ascii(substring(value,2,1)),
pos3 = ascii(substring(value,3,1)),
pos4 = ascii(substring(value,4,1))
) ap;
This produces:
escapedVal
pos1
pos2
pos3
pos4
option1
option2
option3
\u0000\u0000\u0000\u0007
0
0
0
7
7
7
7
\r\t\u000e\b
13
9
14
8
139148
44
13091408
CAST(CAST(#value as BINARY(4)) as INT)
The part I was missing is specifying the size of binary as 4. Without the size, it always casts to 0!

1+1=3? Space characters in nvarchar variables and string lengths

I've just stumbled upon this:
Why doesn't the following code:
DECLARE #s nvarchar(10) = N' '
PRINT CONCAT('#', #s, '#')
PRINT CONCAT('#', LEN(#s), '#')
result in either the output
##
#0#
or
# #
#1#
On a SQL Server 2017, however, this code produces the output
# #
#0#
Which seems contradictory to me.
Either the string has the length 0 and is '' or the length 1 and is ' '.
The whole thing becomes even stranger if you add the following code:
DECLARE #s nvarchar(10) = N' '
PRINT CONCAT('#', #s, '#')
PRINT CONCAT('#', LEN(#s), '#')
DECLARE #l1 int = LEN(CONCAT('#', #s, '#'))
PRINT LEN(#s)
PRINT LEN('#')
PRINT #l1
Which outputs the following:
# #
#0#
0
1
3
So we have three substrings, one with length 0, two with length 1. The total string then has length 3? I'm confused.
If you fill #s with several spaces, it looks even more funny - e.g. 5 spaces results in this output:
# #
#0#
0
1
7
So here's 1×0 + 2×1 even 7. I wish my bank would calculate my account balance like this.
Can someone explain to me what's going on?
Many thanks for your help!
LEN
Returns the number of characters of the specified string expression,
excluding trailing spaces.
So LEN(' ') = 0 (only spaces), but LEN(' x') = 2 (no trailing spaces).
LEN excludes trailing spaces. If that is a problem, consider using the
DATALENGTH (Transact-SQL) function which does not trim the string. If
processing a unicode string, DATALENGTH will return twice the number
of characters.

SQL server string or VARCHAR manipulation containing numerics

In SQL server, I have VARCHAR values.
I need a view that automatically reformats data.
Data that is stored in the following form:
hawthorn104freddy#hawthorn.com
scotland2samantha#gmail.com3
birmingham76roger#outlook.co.uk1905student
Needs to be reformatted into the following:
hawthorn 104freddy#hawthorn.com0000
scotland 002samantha#gmail.com 0003
birmingham076roger#outlook.co.uk1905student
Reformatting
Numeric values within the strings are padded with zeros to the length of the longest number
All other characters are padded with space characters to line up the numbers.
Does anyone know how this is done?
Note: Bear in mind that a string may contain any combination of words and numbers.
You should split your values to 4 columns (to find maximum length in each column), then add leading/trailing zeros/spaces, then concat it.
Here is code to split values, hope you will have no problems with adding zeros and spaces:
declare #v varchar(255) = 'hawthorg104freddy#hawthorn.com50'
select
FirstPart = left(#v, patindex('%[a-z][0-9]%', #v)),
SecondPart = substring(#v, patindex('%[0-9]%', #v), patindex('%[0-9][a-z]%', #v) - patindex('%[a-z][0-9]%', #v)),
ThirdPart = substring(#v, patindex('%[0-9][a-z]%', #v) + 1, len(#v) - patindex('%[0-9][a-z]%', #v) - patindex('%[0-9][a-z]%', reverse(#v))),
Fourthpart = right(#v, patindex('%[0-9][a-z]%', reverse(#v)))
Notes:
patindex('%[a-z][0-9]%', #v) - Last letter in hawthorn (nickname?)
patindex('%[0-9][a-z]%', #v) - Last digit in first number (104)
patindex('%[0-9][a-z]%', reverse(#v)) - Length of the last number
You can also use CLR and RegEx to split values to groups:
https://github.com/zzzprojects/Eval-SQL.NET/wiki/SQL-Server-Regex-%7C-Use-regular-expression-to-search,-replace-and-split-text-in-SQL
You can use PATINDEX
declare #str varchar(100)='hawthorn104freddy#hawthorn.com'
SELECT SUBSTRING(#str,0,PATINDEX('%[0-9]%',#str)),
SUBSTRING(#str,PATINDEX('%[0-9]%',#str),LEN(#str)-LEN(SUBSTRING(#str,0,PATINDEX('%[0-9]%',#str))))

SQL Select command to selectively extract data from a string

I have data in a column which contains values for various fields from an application and all these fields are concatenated into one field on the database side and separated with commas.
If the field in the application is blank, then the value between the two commas will just be blank.
I need a select statement to select each of the individual fields if they are populated. I would like to specify each field as a variable which I will declare at the top of the statement.
An example of the string in the database field is:
,"FIELD1","FIELD2","FIELD3",FIELD4,FIELD5,FIELD6,,,,"FIELD10",FIELD11,FIELD12,FIELD13
As you can see, fields 7-9 were blank in this example so they are blank in the string.
I just need a way to selectively select the field I need using the commas as my marker. The string always starts with a comma so field1 always comes after the first comma.
I hope this makes sense!
Try this:
DECLARE #STRING VARCHAR(255) = ',"FIELD1","FIELD2","FIELD3",FIELD4,FIELD5,FIELD6,,,,"FIELD10",FIELD11,FIELD12,FIELD13'
DECLARE #FieldToReturn INT = 12 -- Pick which field you want
SET #STRING = RIGHT(#STRING, LEN(#STRING) - 1) + ',' -- Strip leading comma & add comma to the end
WHILE #FieldToReturn > 1
BEGIN
SET #STRING = SUBSTRING(#STRING,PATINDEX('%,%',#STRING), LEN(#STRING))
SET #FieldToReturn = #FieldToReturn - 1
SET #STRING = RIGHT(#STRING, LEN(#STRING) - 1)
END
SELECT SUBSTRING(#STRING,0,PATINDEX('%,%', #STRING))
If it the field is not populated, this will return a blank.
Edit: I know that I could have put all of the string manipulation in one line within the WHILE, but chose not to for readability...to me that is more important that the possibility of a teeny tiny bit of overhead in this example

ASCII increment with defined range

Client wants to append a field with a literal increment based on a count.
The range goes from 'aa' to 'zz'.
'aa' represents a count of 1 and 'zz' represents the max value in the range: 676
I have sql that almost works but would appreciate an expert eye to get me over the last hurdle.
--Constants
DECLARE #START_ASCII INT = 97
DECLARE #ASCII_OFFSET INT = 1
DECLARE #ALPHABET_LETTER_COUNT INT = 26
--Variables
DECLARE #RecordCount INT = 0
DECLARE #FirstLetter VARCHAR(1) = NULL
DECLARE #SecondLetter VARCHAR(1) = NULL
SET #RecordCount = 1 --Range is 1 to 676 (e.g. 'aa' to 'zz')
SET #FirstLetter = CHAR(round(#RecordCount / #ALPHABET_LETTER_COUNT, 2, 1) + #START_ASCII)
SET #SecondLetter = CHAR((((#RecordCount - #ASCII_OFFSET) % #ALPHABET_LETTER_COUNT) + #START_ASCII))
SELECT #FirstLetter + #SecondLetter
The problem with the above sql involves the first letter. It works till the end of the alphabet is reached for the second letter. For example, at a count of 26, I expect 'az', but instead get 'bz'.
I want to keep the SQL small and tight (e.g. no CASE statements). Is there a small tweak I can make to the above code so that it will work?
Or, if there is just a smarter way to skin this cat, I'd like to know that.
I would think of this as computing the base-26 representation of #RecordCount-1 (range 0 to 675). Then map the two-digits of the base-26 number to the ASCII characters:
SET #FirstLetter = CHAR(floor((#RecordCount-1) / #ALPHABET_LETTER_COUNT) + #START_ASCII)
SET #SecondLetter = CHAR(((#RecordCount-1) % #ALPHABET_LETTER_COUNT) + #START_ASCII)

Resources