How does SQL Server know what precision to use for money - sql-server

How does SQL Server know to retrieve these values this way?
Key someMoney
----------- ---------------------
1 5.00
2 5.002
3 5.0001
Basically, I'm wondering how to know how many decimal places there are without much of a performance hit.
I want to get
Key someMoney places
----------- --------------------- ----------
1 5.00 2
2 5.002 3
3 5.0001 4

Money has 4 decimal places....it's a fixed-point data type.
http://msdn.microsoft.com/en-us/library/ms179882.aspx
Is SQL Server 'MONEY' data type a decimal floating point or binary floating point?

So this is a huge ugly hack, but it will give you the value you're looking for...
DECLARE #TestValue MONEY
SET #TestValue = 1.001
DECLARE #TestString VARCHAR(50)
SET #TestString = REPLACE(RTRIM(REPLACE(CONVERT(VARCHAR, CONVERT(DECIMAL(10,4), #TestValue)), '0', ' ')), ' ', '0')
SELECT LEN(#TestString) - CHARINDEX('.', #TestString) AS Places

This produces the correct results, but I'm not sure if it performs well enough for you and I haven't tried it with data other than the examples you listed:
;
with money_cte ([Key], [someMoney])
as
(
select 1, cast(5.00 as money)
union
select 2, cast(5.002 as money)
union
select 3, cast(5.0001 as money)
)
select [Key], [someMoney], abs(floor(log10([someMoney] - round([someMoney], 0, 1)))) as places
from money_cte
where [someMoney] - round([someMoney], 0, 1) <> 0
union
select [Key], [someMoney], 2 as places
from money_cte
where [someMoney] - round([someMoney], 0, 1) = 0

The client is formatting that. SQL Server SSMS or whatever. SQL Server is returning a full money value in the data stream and it takes a full 8 bytes. (http://msdn.microsoft.com/en-us/library/cc448435.aspx). If you have SQL Server convert to varchar, it defaults to 2 decimal places
Notice that the Stack Overflow data browser doesn't even show the same results you have:
https://data.stackexchange.com/stackoverflow/q/111288/
;
with money_cte ([Key], [someMoney])
as
(
select 1, cast(5.00 as money)
union
select 2, cast(5.002 as money)
union
select 3, cast(5.0001 as money)
)
select *
, CONVERT(varchar, someMoney) AS varchar_version
, CONVERT(varchar, someMoney, 2) AS varchar_version2
FROM money_cte​

Related

Substring with patindex for substrings' length not working to extract part of a string

I have a table with strings (ItemCode) like:
99XXX123456-789
12ABC221122-987BA1
They are always of a length of 11 characters (upto the - of which they always contain only one), after the - length is variable. I would like to get the part after the first 5 characters upto the - , like this.
123456
221122
I tried with substring and patindex:
SELECT SUBSTRING( ItemCode, 6, PATINDEX('%[-]%', ItemCode) - 6 ),
PATINDEX('%[-]%', ItemCode),
ItemCode
FROM TableName
WHERE LEFT(ItemCode, 5) = '99XXX'
Patindex itself returns the correct value (12) but with PATINDEX('%[-]%', ItemCode) - 6 /sql should understand this as 12 - 6 = 6 / SQL Server 2012 gives an error. I could use 6 as a fix value in the patindex for the length, of course but I want to understand the reason for the error.
But when I am using the same query I am not getting the error:
create table #temp
(
num varchar(50)
)
--insert into #temp values ('99XXX123456-789')
--insert into #temp values ('12ABC221122-987BA1')
SELECT SUBSTRING( num, 6, PATINDEX('%[-]%', num) - 6 ),
PATINDEX('%[-]%', num),
num
FROM #temp WHERE LEFT(num, 5) = '99XXX'

T-SQL / SQL Server : rounding after convert (decimal) SqlCommand

I want to normalize my SQL Server column before adding to datagridview, I did it in my SQL query. But I have a problem.
If the value fully dividing (for example 100/100=1) I don't want to see this value like 1.00000000000000. And if the value is not fully dividing (for example 3/7=0.42857142857), I want to round that value to two digits after rounding (0.43).
This is my code:
string q = "SELECT column1, column2, ((CONVERT(DECIMAL(18, 2), column3) / (SELECT MAX(column3) FROM tablename)) * 100) AS normalizedColumn3
FROM tablename .......;
I need to normalize column 3, it's values before normalize between 1 - 2000000. After normalize, values will be between 0.01 - 100.
Normalize formula:
column 3 = ((column 3 / maximum column 3) * 100)
Thank you for answers...
You'll have a small matter of data-loss if you only want two decimals. You'll need at least 5 decimals for values between 1 and 2,000,000
Example
Declare #YourTable table (Col3 float)
Insert into #YourTable values
(1),(1536),(1000000),(2000000)
Select A.*
,NewVal = convert(decimal(10,2), (Col3*100.0) / ( Select max(Col3) from #YourTable) )
From #YourTable A
Returns
Col3 NewVal
1 0.00 -- At decimal(10,5) you would see 0.00005
1536 0.08
1000000 50.00
2000000 100.00
I believe you can use ROUND ( numeric_expression , length [ ,function ] ) or SELECT ROUND(CAST (# AS decimal (#,#)),#); to round your decimal.
Here is more info on it :https://learn.microsoft.com/en-us/sql/t-sql/functions/round-transact-sql?view=sql-server-2017

Sorting a string numerically in SQL Server

I have a column in a table that is populated with a string
level 1, level 2, level 3 ... level 12.
I wish to order by this column but not alphabetically as this orders the column
1 10 11 12 2 3 4 5 6 7 8 9
How can I order this column to be in correct numerical order despite it being a string type?
I imagine I have to extract only the numerical component of the string and cast it to an int but I don't know how to do this in SQL Server.
Thanks
Try this:
Select *
From YourTable
Order by CAST( SUBSTRING(YourColumn, 6,LEN(YourColumn)) AS Int)
Try this:
SELECT *
FROM TableName
ORDER BY CAST(REPLACE(ColumnName, 'level', '') AS INT)
Try something like this:
SELECT col
FROM
(values('level 1'),('level 2'),('level 3'),
('level 5'),('level 8'),('level 10'),('level 12'))
x(col)
ORDER BY stuff(col, 1,6, '') + 0
The stuff will remove the first 6 characters, the +0 will cast the rest of the col as integer(which can also be done with cast or convert)

time difference of comma-delimited values

I have a requirement from my customer. Data pattern is below for Note Column
:TT: 12:32,12:35 :TT:
:TT: 05:17,05:30 :TT:
:TT: 01:56,02:00 :TT:
:TT: 01:00,01:12 :TT:
I need to remove :TT: tag and subtract first value from last.
for example 12:35-12:32=00:03.
I have applied below code
WITH CTE1 AS
(
SELECT (LTRIM(RTRIM(REPLACE(CAST(Note as NVarchar(4000)),':tt:','')))) AS Note
FROM UD_Notes
WHERE Note like ':t%'
)
SELECT Note FROM CTE1
Now getting below result-
01:00,01:12
01:56,02:00
05:17,05:30
12:32,12:35
Can anyone help me out for solve this?
i have a solution that will give you something like this:
3
13
4
12
Note: Updated it with DATEDIFF
WITH CTE1 AS
(
SELECT (LTRIM(RTRIM(REPLACE(CAST(Note as NVarchar(4000)),':tt:','')))) AS Note
FROM UD_Notes
WHERE Note like ':t%'
)
SELECT Note INTO #RESULT FROM CTE1
SELECT
DATEDIFF(MINUTE, CONVERT(datetime, left(CONVERT(varchar(50), LEFT(Note, CHARINDEX(',', Note) - 1)), 19)), CONVERT(datetime, left(CONVERT(varchar(50), RIGHT(Note, CHARINDEX(',', Note) - 1)), 19)))
FROM #RESULT
I have tested it like this:
CREATE TABLE #PRERESULT(
Note text)
INSERT INTO #PRERESULT (note)
VALUES
(':TT: 12:32,12:35 :TT:'),
(':TT: 05:17,05:30 :TT:'),
(':TT: 01:56,02:00 :TT:'),
(':TT: 01:00,01:12 :TT:');
WITH CTE1 AS
(
SELECT (LTRIM(RTRIM(REPLACE(CAST(Note as NVarchar(4000)),':tt:','')))) AS Note
FROM #PRERESULT
WHERE Note like ':t%'
)
SELECT Note INTO #RESULT FROM CTE1
SELECT
DATEDIFF(MINUTE, CONVERT(datetime, left(CONVERT(varchar(50), LEFT(Note, CHARINDEX(',', Note) - 1)), 19)), CONVERT(datetime, left(CONVERT(varchar(50), RIGHT(Note, CHARINDEX(',', Note) - 1)), 19)))
FROM #RESULT
DROP TABLE #PRERESULT
DROP TABLE #RESULT
Have a nice day
Etienne
You can use an scalar user defined function that receives the string (even the whole string including the ':TT:'), and process it to give you back the desired result.
In this way you can reuse it and test it easyly.
Somethign like this:
CREATE FUNCTION dbo.TimeDiff(#value AS nvarchar(4000))
RETURNS NVARCHAR(5)
AS
BEGIN
DECLARE #cleaned NVARCHAR(11)
SET #cleaned = LTRIM(RTRIM(REPLACE(#value,':TT:','')))
DECLARE #h1 int
DECLARE #m1 int
DECLARE #h2 int
DECLARE #m2 INT
DECLARE #t1 INT
DECLARE #t2 int
SET #h1 = CAST(SUBSTRING(#cleaned,1,2) AS int)
SET #m1 = CAST(SUBSTRING(#cleaned,4,2) AS int)
SET #h2 = CAST(SUBSTRING(#cleaned,7,2) AS int)
SET #m2 = CAST(SUBSTRING(#cleaned,10,2) AS int)
SET #t1 = #h1 * 60 + #m1
SET #t2 = #h2 * 60 + #m2
DECLARE #diff INT
DECLARE #diffh NVARCHAR(2)
DECLARE #diffm NVARCHAR(2)
SET #diff = #t1 - #t2
SET #diffh = RIGHT('0' + CAST(#diff / 60 AS NVARCHAR(2)),2)
SET #diffm = RIGHT('0' + CAST(#diff % 60 AS NVARCHAR(2)), 2)
RETURN #diffh + ':' + #diffm
END
And you can test it like this:
SELECT dbo.TimeDiff(':TT: 12:23,11:54 :TT:')
Then you can easily use this function in your select sentences. Of course, you can modify the implementation to make it more efficient, or modify the details of the calculation.
Try this:
select
right('00'+cast(cast(substring(ltrim(rtrim(replace(cast(Note as nvarchar(100)),':TT:',''))),7,2) as int) - cast(substring(ltrim(rtrim(replace(cast(Note as nvarchar(100)),':TT:',''))),1,2) as int) as varchar),2)
+ ':'
+ right('00'+cast(cast(substring(ltrim(rtrim(replace(cast(Note as nvarchar(100)),':TT:',''))),10,2) as int) - cast(substring(ltrim(rtrim(replace(cast(Note as nvarchar(100)),':TT:',''))),4,2) as int) as varchar),2)
from ud_notes where note like ':t%'
Breakdown:
Replace ':TT:' with blanks.
Trim the string to remove unnecessary spaces.
Since the strings represent time as HH:MM, we can safely assume that the resulting substring will always be of the form xx:xx,xx:xx and of length 11. So, we can hardcode the indices where we get the substrings representing hours and minutes for start and end times.
Cast the strings to integers and perform the subtraction.
Pad with the necessary amount of zeros.
Concatenate with a separator to return the expected output as varchar.
select cast(dateadd(minute,
datediff(minute,
substring(Note, 7, 5),
substring(Note, 13, 5)
),
0) as time(0))
Result:
00:03:00
00:13:00
00:04:00
00:12:00
SQL Fiddle
Test Data
DECLARE #TABLE TABLE (Note TEXT)
INSERT INTO #TABLE VALUES
(':TT: 12:32,12:35 :TT:'),
(':TT: 05:17,05:30 :TT:'),
(':TT: 01:56,02:00 :TT:'),
(':TT: 01:00,01:12 :TT:')
Query
;WITH CTE1 AS
(
SELECT CAST(REPLACE(LEFT(Note, CHARINDEX(',', Note) -1),':tt:','') AS TIME) AS StartTime
,CAST(SUBSTRING(Note, CHARINDEX(',', Note)+ 1, 5) AS TIME) AS EndTime
FROM (SELECT CAST(Note AS VARCHAR(1000)) AS Note FROM #TABLE) A
WHERE Note like ':t%'
)
SELECT CAST(StartTime AS NVARCHAR(5)) AS StartTime
,CAST(EndTime AS NVARCHAR(5)) AS EndTime
,DATEDIFF(MINUTE, StartTime,EndTime) TimeDifference
FROM CTE1
Result Set
╔═══════════╦═════════╦════════════════╗
║ StartTime ║ EndTime ║ TimeDifference ║
╠═══════════╬═════════╬════════════════╣
║ 12:32 ║ 12:35 ║ 3 ║
║ 05:17 ║ 05:30 ║ 13 ║
║ 01:56 ║ 02:00 ║ 4 ║
║ 01:00 ║ 01:12 ║ 12 ║
╚═══════════╩═════════╩════════════════╝
Working SQL FIDDLE
It's taken me a while, and I'll be this isn't the most efficient way of doing it (too many conversions), but I have an XML solution working which someone might be able to improve on. Note that since you are dealing with times I have used Date functions - but make sure you read the caveat at the end. Basically, in your CTE you convert the data to simple XML form like
<Note>
<T1>00:00</T1>
<T2>00:00</T2>
</Note>
This takes away the need to do string splitting functions. Now you can get your result by using a combination of DATEDIFF, DATEADD, CAST and LEFT
WITH CTE1 AS
(
SELECT CAST('<Note><T1>' + REPLACE((LTRIM(RTRIM(REPLACE(CAST(Note AS NVARCHAR(4000)),
':tt:','')))), ',', '</T1><T2>') + '</T2></Note>' AS XML) AS Note
FROM UD_Notes
WHERE Note like ':t%'
)
SELECT LEFT(CAST(DATEADD(mi, DATEDIFF(mi, CAST(y.T1 AS TIME), CAST(y.T2 AS TIME)), CAST('00:00' AS TIME)) AS VARCHAR(20)), 5)
FROM (
SELECT c.value('(T1/text())[1]', 'varchar(50)') AS T1,
c.value('(T2/text())[1]', 'varchar(50)') AS T2
FROM (SELECT Note FROM CTE1) x
CROSS APPLY Note.nodes('/Note') AS T(c)) y
What's happening is that first we get the CTE to give us an XML data type which we can then query to give us two 'columns' (T1 and T2). The SELECT is doing a lot of jobs at once so I'll break it down. Lets assume the example you gave which was 12:35 - 12:32 = 00:03.
DATEDIFF(mi, CAST(y.T1 AS TIME), CAST(y.T2 AS TIME))
Here we get the two columns T1 and T2 and we CAST those to the TIME type and get the difference (i.e. T2 - T1). This gives us an INT difference of 3 so in the next bit I'll simply substitute 3 into the relevant part.
So, now we get
DATEADD(mi, 3, CAST('00:00' AS TIME))
What we're doing here is creating a notional TIME of 00:00 and adding our difference to it in minutes (mi) so this gives us 00:03. Once again, we'll substitute it in.
CAST(00:03 AS VARCHAR(20))
Pretty simple here, we're just getting a VARCHAR representation of our time. Because TIME has a pretty good accuracy, we get the seconds and nanoseconds which would look like this 00:03:00.0000000. We want rid of the seconds and nanoseconds since we're dealing with hours and minutes here.
LEFT('00:03:00.0000000', 5)
That leaves us with '00:03' as a string.
I won't bother going through the body of the query because although I can write it I don't think I could explain it quite clearly enough but there's plenty of pages that will explain XML querying far better than I can such as this blog post from MSDN and this blog post which looks at querying XML fields in t-sql.
I have no doubt there's a better way to do this and someone on here can also give a good explanation of the query - please feel free anyone to add to this and explain what I struggle to.
CAVEAT
If T1 was 01:00 and T2 was 12:57 then you would get an answer of 11:57 because they are indeed 11 hours and 57 minutes apart in the forward direction. To be honest I haven't got time now to work out how to make this give you a result of 3 for those two times, but someone else probably can spot how to, I hope.
Hi Nishant try this one:
select DATEDIFF(MI,SUBSTRING(REPLACE(Note,':TT:',''),
charindex('',REPLACE(Note,':TT:','')),
charindex(',',REPLACE(Note,':TT:',''))),
SUBSTRING(REPLACE(Note,':TT:',''),
charindex(',',REPLACE(Note,':TT:',''))+1,
len(REPLACE(colval,':TT:',''))-charindex(',',REPLACE(Note,':TT:',''))))
from UD_Notes
If you want to get results in hh:mi format:
SELECT
CONVERT(VARCHAR(5), DATEADD(minute, DATEDIFF(minute, LEFT(LTRIM(REPLACE(CONVERT(VARCHAR(8000), Note), ':TT:', '')), 5), RIGHT(RTRIM(REPLACE(CONVERT(VARCHAR(8000), Note), ':TT:', '')), 5)), 0), 14)
FROM
UD_Notes
WHERE
PATINDEX(':T%', Note) > 0
Explanation: Extract the left and right time strings. Implicitly convert the strings to time data while getting the integer difference in minutes. Convert the resulting integer to hh:mi.
Well all answers were quite correct and I have to choose in one of them.
Unfortunately i was getting blanks spaces in result set because of those blank spaces I was getting error of conversion (varchar to time).
I have overcome problem by below query and it is working for me right now.
Thanks all senior members and experts to providing me their valuable inputs.
I got right approach to get my desired result.
{
SELECT
CONVERT(VARCHAR,DATEDIFF(mi,
CAST(SUBSTRING(REPLACE(CAST(Note AS VARCHAR(1000)),':TT:',''),CHARINDEX(':',REPLACE(CAST(Note AS VARCHAR(1000)),':TT:',''))-2,CHARINDEX(':',REPLACE(CAST(Note AS VARCHAR(1000)),':TT:',''))) AS TIME)
,CAST(SUBSTRING(REPLACE(CAST(Note AS VARCHAR(1000)),':TT:',''),CHARINDEX(',',REPLACE(CAST(Note AS VARCHAR(1000)),':TT:',''))+1,5) AS Time))) + ' mins' As 'TimeDiff'
FROM UD_Notes where Note like '%:TT:%' }

Normalizing a table

I have a legacy table, which I can't change.
The values in it can be modified from legacy application (application also can't be changed).
Due to a lot of access to the table from new application (new requirement), I'd like to create a temporary table, which would hopefully speed up the queries.
The actual requirement, is to calculate number of business days from X to Y. For example, give me all business days from Jan 1'st 2001 until Dec 24'th 2004. The table is used to mark which days are off, as different companies may have different days off - it isn't just Saturday + Sunday)
The temporary table would be created from a .NET program, each time user enters the screen for this query (user may run query multiple times, with different values, table is created once), so I'd like it to be as fast as possible. Approach below runs in under a second, but I only tested it with a small dataset, and still it takes probably close to half a second, which isn't great for UI - even though it's just the overhead for first query.
The legacy table looks like this:
CREATE TABLE [business_days](
[country_code] [char](3) ,
[state_code] [varchar](4) ,
[calendar_year] [int] ,
[calendar_month] [varchar](31) ,
[calendar_month2] [varchar](31) ,
[calendar_month3] [varchar](31) ,
[calendar_month4] [varchar](31) ,
[calendar_month5] [varchar](31) ,
[calendar_month6] [varchar](31) ,
[calendar_month7] [varchar](31) ,
[calendar_month8] [varchar](31) ,
[calendar_month9] [varchar](31) ,
[calendar_month10] [varchar](31) ,
[calendar_month11] [varchar](31) ,
[calendar_month12] [varchar](31) ,
misc.
)
Each month has 31 characters, and any day off (Saturday + Sunday + holiday) is marked with X. Each half day is marked with an 'H'. For example, if a month starts on a Thursday, than it will look like (Thursday+Friday workdays, Saturday+Sunday marked with X):
' XX XX ..'
I'd like the new table to look like so:
create table #Temp (country varchar(3), state varchar(4), date datetime, hours int)
And I'd like to only have rows for days which are off (marked with X or H from previous query)
What I ended up doing, so far is this:
Create a temporary-intermediate table, that looks like this:
create table #Temp_2 (country_code varchar(3), state_code varchar(4), calendar_year int, calendar_month varchar(31), month_code int)
To populate it, I have a union which basically unions calendar_month, calendar_month2, calendar_month3, etc.
Than I have a loop which loops through all the rows in #Temp_2, after each row is processed, it is removed from #Temp_2.
To process the row there is a loop from 1 to 31, and substring(calendar_month, counter, 1) is checked for either X or H, in which case there is an insert into #Temp table.
[edit added code]
Declare #country_code char(3)
Declare #state_code varchar(4)
Declare #calendar_year int
Declare #calendar_month varchar(31)
Declare #month_code int
Declare #calendar_date datetime
Declare #day_code int
WHILE EXISTS(SELECT * From #Temp_2) -- where processed = 0)
BEGIN
Select Top 1 #country_code = t2.country_code, #state_code = t2.state_code, #calendar_year = t2.calendar_year, #calendar_month = t2.calendar_month, #month_code = t2.month_code From #Temp_2 t2 -- where processed = 0
set #day_code = 1
while #day_code <= 31
begin
if substring(#calendar_month, #day_code, 1) = 'X'
begin
set #calendar_date = convert(datetime, (cast(#month_code as varchar) + '/' + cast(#day_code as varchar) + '/' + cast(#calendar_year as varchar)))
insert into #Temp (country, state, date, hours) values (#country_code, #state_code, #calendar_date, 8)
end
if substring(#calendar_month, #day_code, 1) = 'H'
begin
set #calendar_date = convert(datetime, (cast(#month_code as varchar) + '/' + cast(#day_code as varchar) + '/' + cast(#calendar_year as varchar)))
insert into #Temp (country, state, date, hours) values (#country_code, #state_code, #calendar_date, 4)
end
set #day_code = #day_code + 1
end
delete from #Temp_2 where #country_code = country_code AND #state_code = state_code AND #calendar_year = calendar_year AND #calendar_month = calendar_month AND #month_code = month_code
--update #Temp_2 set processed = 1 where #country_code = country_code AND #state_code = state_code AND #calendar_year = calendar_year AND #calendar_month = calendar_month AND #month_code = month_code
END
I am not an expert in SQL, so I'd like to get some input on my approach, and maybe even a much better approach suggestion.
After having the temp table, I'm planning to do (dates would be coming from a table):
select cast(convert(datetime, ('01/31/2012'), 101) -convert(datetime, ('01/17/2012'), 101) as int) - ((select sum(hours) from #Temp where date between convert(datetime, ('01/17/2012'), 101) and convert(datetime, ('01/31/2012'), 101)) / 8)
Besides the solution of normalizing the table, the other solution I implemented for now, is a function which does all this logic of getting the business days by scanning the current table. It runs pretty fast, but I'm hesitant to call a function, if I can instead add a simpler query to get result.
(I'm currently trying this on MSSQL, but I would need to do same for Sybase ASE and Oracle)
This should fulfill the requirement, "...calculate number of business days from X to Y."
It counts each space as a business day and anything other than an X or a space as a half day (should just be H, according to the OP).
I pulled this off in SQL Server 2008 R2:
-- Calculate number of business days from X to Y
declare #start date = '20120101' -- X
declare #end date = '20120101' -- Y
-- Outer query sums the length of the full_year text minus non-work days
-- Spaces are doubled to help account for half-days...then divide by two
select sum(datalength(replace(replace(substring(full_year, first_day, last_day - first_day + 1), ' ', ' '), 'X', '')) / 2.0) as number_of_business_days
from (
select
-- Get substring start value for each year
case
when calendar_year = datepart(yyyy, #start) then datepart(dayofyear, #start)
else 1
end as first_day
-- Get substring end value for each year
, case
when calendar_year = datepart(yyyy, #end) then datepart(dayofyear, #end)
when calendar_year > datepart(yyyy, #end) then 0
when calendar_year < datepart(yyyy, #start) then 0
else datalength(full_year)
end as last_day
, full_year
from (
select calendar_year
-- Get text representation of full year
, calendar_month
+ calendar_month2
+ calendar_month3
+ calendar_month4
+ calendar_month5
+ calendar_month6
+ calendar_month7
+ calendar_month8
+ calendar_month9
+ calendar_month10
+ calendar_month11
+ calendar_month12 as full_year
from business_days
-- where country_code = 'USA' etc.
) as get_year
) as get_days
A where clause can go on the inner-most query.
It is not an un-pivot of the legacy format, which the OP spends much time on and which will probably take more (and possibly unnecessary) computing cycles. I'm assuming such a thing was "nice to see" rather than part of the requirements. Jeff Moden has great articles on how a tally table could help in that case (for SQL Server, anyway).
It might be necessary to watch trailing spaces depending upon how a particular DBMS is set (notice that I'm using datalength and not len).
UPDATE: Added the OP's requested temp table:
select country_code
, state_code
, dateadd(d, t.N - 1, cast(cast(a.calendar_year as varchar(8)) as date)) as calendar_date
, case substring(full_year, t.N, 1) when 'X' then 0 when 'H' then 4 else 8 end as business_hours
from (
select country_code
, state_code
, calendar_year
, calendar_month
+ calendar_month2
+ calendar_month3
+ calendar_month4
+ calendar_month5
+ calendar_month6
+ calendar_month7
+ calendar_month8
+ calendar_month9
+ calendar_month10
+ calendar_month11
+ calendar_month12
as full_year
from business_days
) as a, (
select a.N + b.N * 10 + c.N * 100 + 1 as N
from (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) a
, (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) b
, (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) c
) as t -- cross join with Tally table built on the fly
where t.N <= datalength(a.full_year)
Given your temp table is slow to create, are you able to pre-calculate it?
If you're able to put a trigger on the existing table, perhaps you could fire a proc which will drop and create the temp table. Or have an agent job which checks to see if the existing table has been updated (raise a flag somewhere) and then recomputes the temp table.
The existing table's structure is so woeful that I wouldn't be surprised if it will always be expensive to normalize it. Pre-calculating is an easy and simple way around that problem.

Resources