Formatting an SQL numeric query result with an arbitrary number of decimal places - sql

I have a database table with these two columns:
Amount: numeric (18,0)
DecimalPlaces: numeric (18,0)
This table can store amounts in various currencies, with the decimal place removed from the amount (I can't change this data model). For example, there might be two rows like this:
1290, 2 (This is £12.90, needs to appear as "12.90")
3400, 0 (This is 3400 Japanese Yen, needs to appear as "3400")
I need an SQL query for both Oracle and SQL Server that will format each amount with the correct number of decimal places, preserving any trailing zeroes as illustrated above. I can't use stored procedures, a reporting tool, or Excel.

Your problem is that there isn't an easy way to do this for both SQLServer and Oracle in one query.
The Correct way to do this for SQLServer is to use STR:
Select STR(Amount, 18, DecimalPlaces) from myTable;
The correct way to do this for Oracle is using to_char:
SELECT to_char (amount, '99999999999999.'||rpad('',DecimalPlaces, '0'))
from MyTable;
The queries presented by jms and Andrew won't work in an Oracle query because Oracle SQL uses LENGTH() not LEN(). And Oracle uses to_char() not Cast().

The best I've been able to come up with so far is:
select Amount/power(10, DecimalPlaces) from MyTable
But it doesn't do exactly what I want:
Oracle: the trailing zeroes are stripped, so US$15.00 looks like "15", not "15.00"
SQL Server: a whole lot of extra trailing zeroes are added, so $23.99 looks like "23.99000000000" instead of "23.99"

How about?
select 12345 amount, 2 decimalPlaces, substr( to_char( 12345 ), 1, length (to_char( 12345 ) ) - 2 )
|| '.' || substr( to_char( 12345 ), -2 ) result from dual
/
amount decimalPlaces result
---------- ------------- ------
12345 2 123.45

This is gross but worked for the current inputs on SQL server.
select
substring(
CAST(
CAST(
(amount * power(-0.100000000000000000,decimalPlaces*1.000000000000000000)) as numeric(36,18)
)as varchar(30)
)
,1,len(cast(amount as varchar(20))) + (CASE WHEN decimalPlaces = 0 THEN 0 ELSE 1 END ))
from
myTable

In SQL server you can :
select stuff(convert(varchar,amount) ,
len(convert(varchar,amount)) - DecimalPlaces - 1, 0, ".")

Martlark's answer for Oracle led me to this solution for SQL Server:
select
left(cast(Amount as varchar), len(cast(Amount as varchar)) - DecimalPlaces) +
left('.', DecimalPlaces) +
right(cast(OriginalCurrencyAmount as varchar), DecimalPlaces
) as FormattedAmount
from MyTable

Related

CAST - Make string length to be 2 characters long

I need to combine two fields but force the characters of the second string to be 2 characters.
I'm combining a year field and month field and want the result to be YYYY_MM. Forcing any single months (e.g. 1,2,3,4) into a two digit format e.g. (01).
Below is my formula for combining the fields, but I need help making the month two digits.
Thanks, L
WITH so_header(soh_build_year,soh_build_week) AS (
SELECT 2020, 3
UNION ALL SELECT 2020,13
)
SELECT
CAST(SO_HEADER.SOH_Build_Year AS VARCHAR)
+'_'
+CAST(SO_HEADER.SOH_Build_Week AS VARCHAR) as [Build YYYY_WW]
FROM so_header;
Try this out (Syntax: SQL Server)
SELECT
CAST(2019 AS VARCHAR)
+'_'
+CAST(format (1, '0#') AS VARCHAR) as [Build YYYY_WW]
Replace your values with your variables
Try this:
WITH so_header(soh_build_year,soh_build_week) AS (
SELECT 2020, 3
UNION ALL SELECT 2020,13
)
SELECT
CAST(SO_HEADER.SOH_Build_Year AS VARCHAR)
+ '_'
+ SUBSTR(
CAST(100+SO_HEADER.SOH_Build_Week AS VARCHAR)
, 2
, 2
) as Build_YYYY_WW
FROM so_header;
-- out Build_YYYY_WW
-- out ---------------
-- out 2020_03
-- out 2020_13
If you are using SQL Server never use varchar (or related types) with no length. The default varies by context and may not be large enough for what you want.
If you are trying to convert a date to YYYY_MM format, you can use format():
select format(getdate(), 'yyyy_MM')
I recommend using dates, if they are available. If you are not using SQL Server, most other databases have similar functionality.
If not, you an simply use:
select concat(so_header.SOH_Build_Year, '_'
right(concat('00', so_header.soh_build_week), 2)
)
concat() does not require explicitly converting the values to strings.

How to round a number including least significant zeros?

I am trying to execute following SQL query in Oracle
Select round ( 123.50000065 , 4 ) from dual;
Output : 123.5
Required output: 123.5000
Any help is appreciated. ..
You probably want to use to_char with required format:
Below rounds the value to 4 decimal places and formats into the required string:
Select to_char(123.50000065, '999999999990D9999') x from dual;
If you don't want to actually round the number i.e. you just want to truncate after 4 digits, use:
Select to_char(trunc(123.50000065, 4), '999999999990D9999') x from dual;
ROUND ( numeric_expression , length [ ,function ] )
SELECT ROUND(123.9994, 3), ROUND(123.9995, 3);
Output:
123.9990 124.0000
Instead of round(), use to_char() or cast() to a decimal type:
select to_char(val, '999.9999'),
cast(val as decimal(10, 4))
To control the format a number is showed, you can cast it to a string, by applying the right format mask.
Depending on how you need round your input value, one of these could be useful:
with test(x) as (
select 123.50000065 from dual union all
select 123.00004 from dual union all
select 123.00005 from dual union all
select 123.00008 from dual
)
select x,
to_char(x, 'FM99999999.0000'),
to_char(trunc(x, 4), 'FM99999999.0000')
from test ;
result:
X TO_CHAR(X,'FM9 TO_CHAR(TRUNC(
-------------------------- -------------- --------------
123,50000065000 123.5000 123.5000
123,00004000000 123.0000 123.0000
123,00005000000 123.0001 123.0000
123,00008000000 123.0001 123.0000
"Rounding" is a mathematical concept. The value (with your sample input) is 123.5. Mathematically 123.5000 is the same thing as 123.5. They are only different as STRINGS.
One way to display 123.5 as 123.5000 is to wrap round() within to_char(). However, this means you are not able to use it in further computations (actually Oracle will allow you to - it will do an implicit conversion back to number instead of throwing a data type mismatch error, as it should do).
The better way, in most cases, is to address formatting in your client software, like SQL Developer, SQL*Plus, or Toad. Here is how you can do it in SQL*Plus:
SQL> Select round ( 123.50000065 , 4 ) as result from dual;
RESULT
----------
123.5
-- change the format of the numeric column "result"
SQL> column result format 999.0000
SQL> Select round ( 123.50000065 , 4 ) as result from dual;
RESULT
---------
123.5000
I can't see how you got 123.5 from your query. mine results 123.50000000
if I understand correctly, you want your number 4 significant decimal places.
why not try cast
select cast(123.50000065 as numeric(38,4))
output: 123.5000
testing if it rounds off number:
select cast(123.50000065 as numeric(38,6))
output: 123.500001

Date arithmetic in SQL on DB2/ODBC

I'm building a query against a DB2 database, connecting through the IBM Client Access ODBC driver. I want to pull fields that are less than 6 days old, based on the field 'a.ofbkddt'... the problem is that this field is not a date field, but rather a DECIMAL field, formatted as YYYYMMDD.
I was able to break down the decimal field by wrapping it in a call to char(), then using substr() to pull the year, month and day fields. I then formatted this as a date, and called the days() function, which gives a number that I can perform arithmetic on.
Here's an example of the query:
select
days( current date) -
days( substr(char(a.ofbkddt),1,4) concat '-' -- YYYY-
concat substr(char(a.ofbkddt),5,2) concat '-' -- MM-
concat substr(char(a.ofbkddt),7,2) ) as difference, -- DD
a.ofbkddt as mydate
from QS36F.ASDF a
This yields the following:
difference mydate
2402 20050402
2025 20060306
...
4 20110917
3 20110918
2 20110919
1 20110920
This is what I expect to see... however when I use the same logic in the where clause of my query:
select
days( current date) -
days( substr(char(a.ofbkddt),1,4) concat '-' -- YYYY-
concat substr(char(a.ofbkddt),5,2) concat '-' -- MM-
concat substr(char(a.ofbkddt),7,2) ) as difference, -- DD
a.ofbkddt as mydate
from QS36F.ASDF a
where
(
days( current date) -
days( substr(char(a.ofbkddt),1,4) concat '-' -- YYYY-
concat substr(char(a.ofbkddt),5,2) concat '-' -- MM
concat substr(char(a.ofbkddt),7,2) ) -- DD
) < 6
I don't get any results back from my query, even though it's clear that I am getting date differences of as little as 1 day (obviously less than the 6 days that I'm requesting in the where clause).
My first thought was that the return type of days() might not be an integer, causing the comparison to fail... according to the documentation for days() found at http://publib.boulder.ibm.com/iseries/v5r2/ic2924/index.htm?info/db2/rbafzmst02.htm, it returns a bigint. I cast the difference to integer, just to be safe, but this had no effect.
You're going about this backwards. Rather than using a function on every single value in the table (so you can compare it to the date), you should pre-compute the difference in the date. It's costing you resources to run the function on every row - you'd save a lot if you could just do it against CURRENT_DATE (it'd maybe save you even more if you could do it in your application code, but I realize this might not be possible). Your dates are in a sortable format, after all.
The query looks like so:
SELECT ofbkddt as myDate
FROM QS36F.ASDF
WHERE myDate > ((int(substr(char(current_date - 6 days, ISO), 1, 4)) * 10000) +
(int(substr(char(current_date - 6 days, ISO), 6, 2)) * 100) +
(int(substr(char(current_date - 6 days, ISO), 9, 2))))
Which, when run against your sample datatable, yields the following:
myDate
=============
20110917
20110918
20110919
20110920
You might also want to look into creating a calendar table, and add these dates as one of the columns.
What if you try a common table expression?
WITH A AS
(
select
days( current date) -
days( substr(char(a.ofbkddt),1,4) concat '-' -- YYYY-
concat substr(char(a.ofbkddt),5,2) concat '-' -- MM-
concat substr(char(a.ofbkddt),7,2) ) as difference, -- DD
a.ofbkddt as mydate
from QS36F.ASDF a
)
SELECT
*
FROM
a
WHERE
difference < 6
Does your data have some nulls in a.ofbkddt? Maybe this is causing some funny behaviour in how db2 is evaluating the less than operation.

How to give select digit after decimal point?

In database there is column amount which datatype is money. I want to select that row only with two digit after decimal. for this how to write the query?
My query is like this:
SELECT AMOUNT FROM DETAIL_PAGE.
I want to modify this query so that it selects two digits after decimal point.
SELECT AMOUNT - FLOOR(AMOUNT) FROM DETAIL_PAGE
That will get you just the decimal though. I think you want
SELECT FORMAT(AMOUNT, 2) FROM DETAIL_PAGE
Or without commas:
SELECT REPLACE(FORMAT(AMOUNT, 2), ',', '') FROM DETAIL_PAGE
Not sure if this is SQL standard and works elsewhere, but in Oracle you can say
select round(amount,2) from detail_page
-- round(12.345, 2) would return 12.35
or
select trunc(amount,2) from detail_page
-- trunc(12.345, 2) would return 12.34

SQL Server 2008: Varchar Conversion to Numeric Data Overflow, Probably Because Some Are Ranges

I'm working on a query with a varchar column called ALCOHOL_OZ_PER_WK. Part of the query includes:
where e.ALCOHOL_OZ_PER_WK >= 14
and get the errors:
Arithmetic overflow error converting varchar to data type numeric.
and:
Error converting data type varchar to numeric.
Looking into the values actually stored in the column, the largest look close to 100, but some of the entries are ranges:
9 - 12
1.5 - 2.5
I'd like to get the upper limit (or maybe the midpoint of the range) from rows with entries like this and have it be the value being compared to 14.
What would be the (or an) easy way to do this?
As always, thank you!
Your DB is obviously result of some survey, and it seems to contain the original survey data. The usual way is to run this through an ECCD (Extract, Clean, Conform, Deliver) process and store clean and standardized data into a separate database (maybe a warehouse) which can then be used for analytics and reporting.
If you have SSIS use data profiling task to get an idea of types of strings you have in there. The Column Pattern Profile reports a set of regular expressions on the string column, so you will get an idea of what's inside those strings. If you do not have SSIS, you can use eobjects DataCleaner to do the same.
If you can not spare a new database or at least a new table -- at minimum add a numeric column to this table and then extract numeric values form those strings into the new column. You may want to use "something else" (SSIS, Pentaho Kettle, Python, VB, C#) to do this -- in general T-SQL in not very good at string processing.
My guess is that this is not the only column that has garbage inside, so any analysis that you may run on this may be worthless.
And if you still think that the ranges are the only problem, this example may help:
First some data
DECLARE #myTable TABLE (
AlUnits varchar(10)
) ;
INSERT INTO #myTable
(AlUnits )
VALUES ( '10' )
, ( '15' )
, ( '20' )
, ( '7 - 12' )
, ( '3 - 5' )
;
The query splits records into two groups, numeric and not numeric -- assumed ranges.
;
WITH is_num
AS ( SELECT CAST(AlUnits AS decimal(6, 2)) AS Units_LO
,CAST(AlUnits AS decimal(6, 2)) AS Units_HI
FROM #myTable
WHERE ISNUMERIC(AlUnits) = 1
),
is_not_num
AS ( SELECT CAST( RTRIM(LTRIM(LEFT(AlUnits,
CHARINDEX('-', AlUnits) - 1)))
AS decimal(6,2)) AS Units_LO
,CAST(RTRIM(LTRIM(RIGHT(AlUnits,
LEN(AlUnits)
- CHARINDEX('-', AlUnits))))
AS decimal(6,2)) AS Units_HI
FROM #myTable
WHERE ISNUMERIC(AlUnits) = 0
)
SELECT Units_LO
,Units_HI
,CAST(( Units_LO + Units_HI ) / 2.0 AS decimal(6, 2)) AS Units_Avg
FROM is_num
UNION ALL
SELECT Units_LO
,Units_HI
,CAST(( Units_LO + Units_HI ) / 2.0 AS decimal(6, 2)) AS Units_Avg
FROM is_not_num ;
Returns:
Units_LO Units_HI Units_Avg
----------- ----------- ----------
10.00 10.00 10.00
15.00 15.00 15.00
20.00 20.00 20.00
7.00 12.00 9.50
3.00 5.00 4.00
Not sure about easy ways.
A proper way is to store the numbers in two columns, ALCOHOL_OZ_PER_WK_MIN and ALCOHOL_OZ_PER_WK_MAX.
As you say you need to calculate numeric values, which you can then use in your query.
Probably the easiest way is to use some simple logic to calculate the average or upper limit using string functions, and string to numeric functions.
If all you want is the upper limit, just get the characters after the '-' and use that.
"probably because some are ranges" - do you get that "range" is not a SQL Server Data type? You've got non-numeric data you're trying to convert into numeric data, and you've got a scalar value you're comparing to a non-scalar value.
This database has some issues.