Oracle: elegant way to parse a number to 9,99 format - oracle

With Oracle 11g I want to parse a number to strip decimals if their value is 0 and keep two decimal figure after the decimal separator ',' if the value of decimals is different from 0
Example:
1,00 -> 1
1,001 -> 1
0,203 -> 0,20
And so on.
I've obtained something like that in a very unelegant way
select replace(trim(to_char (trunc ('0,2345',2),'9999999990.99')), '.', ',')
from dual
Do you know more elegant way? The output should be a char (not number).

Not sure it's much more elegant, but assuming your replace is to deal with different locales, this might work for you:
with t as (
select 1.00 as n from dual
union all select 1.001 from dual
union all select 0.203 from dual
union all select 0.2345 from dual
union all select 112.999 from dual
)
select n, regexp_replace(to_char(trunc(n, 2), '9999999990D00',
'NLS_NUMERIC_CHARACTERS='',.'''), '[,.]00$', null) as new_n
from t;
N NEW_N
---------- --------------
1 1
1.001 1
0.203 0,20
0.2345 0,23
112.999 112,99
The nls_param argument to to_char let's you dictate whether it used a comma or a period as the decimal separator. If you can set that at session level then the query looks a bit simpler. The regexp_replace strips ,00 (or .00, which come to think of it is overkill) from the end of th string.
As ThinkJet noted the regexp_replace is a bit excessive, and since the decimal seperator is defined in the column clase (and the format has no group separators anyway) it can be done with a plan replace:
with t as (
select 1.00 as n from dual
union all select 1.001 from dual
union all select 0.203 from dual
union all select 0.2345 from dual
union all select 112.999 from dual
union all select 13.08 from dual
)
select n, replace(trim(
to_char(trunc(n, 2), '9999999990D00', 'NLS_NUMERIC_CHARACTERS='',.''')),
',00', null) as new_n
from t;
N NEW_N
---------- --------------
1 1
1.001 1
0.203 0,20
0.2345 0,23
112.999 112,99
13.08 13,08
Still not sure this can be described as 'elegant' though.

To achieve correct results you must deal with numbers, not strings:
with t as (
select 1.00 as n from dual
union all select 1.001 from dual
union all select 0.203 from dual
union all select 0.2345 from dual
union all select 112.999 from dual
union all select 112.105 from dual
union all select 0 from dual
union all select -12.307 from dual
)
select
n,
decode( trunc(n,2) - trunc(n) ,
0, to_char(trunc(n), 'TM9', 'NLS_NUMERIC_CHARACTERS = '', '''),
to_char(trunc(n,2),'9999999990D00', 'NLS_NUMERIC_CHARACTERS = '', ''')
)
string_val
from t
SQLFiddle
P.S. Updated to get incorrect truncation instead of round, as in OP request.

Another variant
SELECT n,
to_char( trunc( n, 2 ),
case when mod( trunc( n, 2 ), 1 ) = 0
then '9990'
else '9990D00'
end, 'NLS_NUMERIC_CHARACTERS='',.''' ) val
from t
;
SQLFiddle demo

Related

PL SQL format number

Using PL SQL, I need to format number to VARCHAR2. I need to show decimal places if they are not zero. And decimal separator should be comma.
INPUT
OUTPUT
0.2
'0,2'
100.4
'100,4'
22
'22'
Would something like this do?
SQL> with test (input) as
2 (select 0.2 from dual union all
3 select 100.4 from dual union all
4 select 22 from dual
5 )
6 select input,
7 case when input = trunc(input) then to_char(input, '999G990')
8 else to_char(input, '999G990D0')
9 end output
10 from test;
INPUT OUTPUT
---------- ----------
,2 0,2
100,4 100,4
22 22
SQL>
this is locale independent, does not limit or round numbers
with
test(input) as (
select 0.2 from dual union all
select 100.4 from dual union all
select -12.34567 from dual union all
select 1.23e34 from dual union all
select -1.23e-34 from dual union all
select 22 from dual
)
select input
,case when input < 0 then '-' end -- sign
||case when abs(input) < 1 then '0' end -- zero
||replace(to_char(abs(input)),to_char(0,'fmd'),',') -- comma
output
from test
/

Replace string in oracle

I have a number column, I need to replace the first number by 7 in oracle.
How to replace guys?
number want_number
4789654 7789654
2754678 7754678
1765689 7765689
For instance
REGEXP_REPLACE(number, '^\d', '7')
should work.
Or, a substring option with concatenation:
SQL> with test (num) as
2 (select 4789654 from dual union all
3 select 2754678 from dual union all
4 select 1765689 from dual
5 )
6 select num, '7' || substr(num, 2) wanted_num
7 from test;
NUM WANTED_NUM
---------- --------------------
4789654 7789654
2754678 7754678
1765689 7765689
SQL>
You could manipulate as numbers, rather than converting to (and presumably later back from) strings:
with your_table (original) as (
select 4789654 from dual
union all select 2754678 from dual
union all select 1765689 from dual
union all select 999 from dual
union all select 1000 from dual
union all select 1001 from dual
)
select original,
original
- trunc(original, -floor(log(10, original)))
+ 7 * power(10, floor(log(10, original))) as wanted
from your_table;
ORIGINAL WANTED
---------- ----------
4789654 7789654
2754678 7754678
1765689 7765689
999 799
1000 7000
1001 7001
The floor(log(10, original) gives you the magnitude of the number. As an example, for your first original value 4789654 that evaluates to 6. If you then do trunc(original, -floor(log(10, original))) that is trunc(4789654, -6), which zeros the six least significant digits, giving you 4000000. Subtracting that from the original value gives you 789654. Then power(10, floor(log(10, original))) gives you power(10, 6) which is 1000000, multiplying that by 7 gives you 7000000, and adding that back on gives you 7789654.
(This won't work if your original value is <= zero, but that looks unlikely?)

Grouping and aggregation based on a specific condition

My result set from a query looks like this
trackingnumber type price
------------------------------------------
12799467 AVRM 674.0536
12799467 AVRM 860.7415
12799467 PRICESTD 200.00
12799468 PRICESTD 590.85
12799469 PRICESTD 800
12799470 PRICESTD 640
12799471 PRICESTD 160
12799472 PRICESTD 2080
12799473 PRICESTD 354.3779
I want to group this by the trackingnumber and in cases where the count of grouped result set is greater than 1 return the SUM of all the price which has type as AVRM else return the individual price as it is. If the count
is more that zero and the none of them has type AVRM then it's total price would be null
The expected result would be this
trackingnumber Total Price
-----------------------------------------
12799467 1534.7951 --sum of price excluding 200
12799468 590.85
12799469 800
12799470 640
12799471 160
12799472 2080
12799473 354.3779
I couldn't think of a way to get this done except for grouping by trackingnumber and checking for type by using case statement in the select part but that I believe would not work since we do not group by type
I'm not sure if this can be achieved using a single query.
Yes, it can be done in a single query.
with sample_data ( tracking_Number, "TYPE", price ) as
(
SELECT 12799467,'AVRM',674.0536 FROM DUAL UNION ALL
SELECT 12799467,'AVRM',860.7415 FROM DUAL UNION ALL
SELECT 12799467,'PRICESTD',200.00 FROM DUAL UNION ALL
SELECT 12799468,'PRICESTD',590.85 FROM DUAL UNION ALL
SELECT 12799469,'PRICESTD',800 FROM DUAL UNION ALL
SELECT 12799470,'PRICESTD',640 FROM DUAL UNION ALL
SELECT 12799471,'PRICESTD',160 FROM DUAL UNION ALL
SELECT 12799472,'PRICESTD',2080 FROM DUAL UNION ALL
SELECT 12799473,'PRICESTD',354.3779 FROM DUAL )
SELECT tracking_number,
case when count(*) > 1 THEN
sum(decode("TYPE",'AVRM',price,null)) ELSE
sum(price) END price
from sample_data
group by tracking_number
order by tracking_Number;
For example next solution. I add condition to exlude rows with type not equla 'AVRM' if rows with 'AVRM' exests
with s (trackingnumber ,type ,price)
as (
select 12799467,'AVRM',674.0536 from dual union all
select 12799467 ,'AVRM', 860.7415 from dual union all
select 12799467 ,'PRICESTD', 200.00 from dual union all
select 12799468 ,'PRICESTD', 590.85 from dual union all
select 12799469 ,'PRICESTD', 800 from dual union all
select 12799470 ,'PRICESTD', 640 from dual union all
select 12799471 ,'PRICESTD', 160 from dual union all
select 12799472 ,'PRICESTD', 2080 from dual union all
select 12799473 ,'PRICESTD', 354.3779 from dual )
select trackingnumber,
sum(price)
from (select s.*,rownum as rn from s
where not exists (select null
from s subs
where s.trackingnumber = subs.trackingnumber
and s.type != 'AVRM'
and subs.type = 'AVRM')
)
group by trackingnumber,
case when type = 'AVRM' then 0 else rn end;

Oracle/Hibernate Like: Finding String that only contain a single ';'

I'm trying to find all the records that contains only a single ; in a column.
For example
a;sdasd
as;dasd;dasd
as;dasd;das
only a;sdasd will be returned.
I have tried %;% but it will return all the strings that contain ;.
SELECT id
FROM your_table
WHERE LENGTH(col) - LENGTH(REPLACE(col,';')) = 1;
Is one solution.
Another uses REGEXP_LIKE:
WITH q AS (SELECT 1 ID, 'a;b;c;' str FROM dual
UNION
SELECT 2, ';abc' FROM dual
UNION
SELECT 3, 'a;b;c;defg;h' FROM dual
UNION
SELECT 4, 'abcdefghi;' FROM dual
UNION
SELECT 5, 'ab;cde' FROM dual
UNION
SELECT 6, 'abcdef' FROM dual)
SELECT *
FROM q
WHERE regexp_like(str,'^[^;]*;[^;]*$');
ID STR
---------- ------------
2 ;abc
4 abcdefghi;
5 ab;cde
Ine other possibility (no String manipulation is necessary):
FROM your_table
WHERE col like '%;%' AND INSTR(col, ';', 1) = INSTR(col, ';', -1)

"Safe" TO_NUMBER()

SELECT TO_NUMBER('*') FROM DUAL
This obviously gives me an exception:
ORA-01722: invalid number
Is there a way to "skip" it and get 0 or NULL instead?
The whole issue: I have NVARCHAR2 field, which contains numbers and not almost ;-) (like *) and I need to select the biggest number from the column.
Yes, I know it is a terrible design, but this is what I need now... :-S
UPD:
For myself I've solved this issue with
COALESCE(TO_NUMBER(REGEXP_SUBSTR(field, '^\d+')), 0)
From Oracle Database 12c Release 2 you could use TO_NUMBER with DEFAULT ... ON CONVERSION ERROR:
SELECT TO_NUMBER('*' DEFAULT 0 ON CONVERSION ERROR) AS "Value"
FROM DUAL;
Or CAST:
SELECT CAST('*' AS NUMBER DEFAULT 0 ON CONVERSION ERROR) AS "Value"
FROM DUAL;
db<>fiddle demo
COALESCE(TO_NUMBER(REGEXP_SUBSTR(field, '^\d+(\.\d+)?')), 0)
will also get numbers with scale > 0 (digits to the right of the decimal point).
I couldn't find anything better than this:
function safe_to_number(p varchar2) return number is
v number;
begin
v := to_number(p);
return v;
exception when others then return 0;
end;
select COALESCE(TO_NUMBER(REGEXP_SUBSTR( field, '^(-|+)?\d+(\.|,)?(\d+)?$')), 0) from dual;
It will convert 123 to 123, but 123a or 12a3 to 0.
Fitting the original question and rather old skool
select a, decode(trim(translate(b,'0123456789.',' ')),null,to_number(b),0) from
(
select '1' a, 'not a number' b from dual
union
select '2' a, '1234' b from dual
)
It's probably a bit messy rolling your own regexp to test for a number, but the code below might work. I think the other solution by Gabe involving a user defined function is more robust since you are using the built in Oracle functionality (and my regexp is probably not 100% correct) but it might be worth a go:
with my_sample_data as (
select '12345' as mynum from dual union all
select '54-3' as mynum from dual union all
select '123.4567' as mynum from dual union all
select '.34567' as mynum from dual union all
select '-0.3462' as mynum from dual union all
select '0.34.62' as mynum from dual union all
select '1243.64' as mynum from dual
)
select
mynum,
case when regexp_like(mynum, '^-?\d+(\.\d+)?$')
then to_number(mynum) end as is_num
from my_sample_data
This will then give the following output:
MYNUM IS_NUM
-------- ----------
12345 12345
54-3
123.4567 123.4567
.34567
-0.3462 -0.3462
0.34.62
1243.64 1243.64
select DECODE(trim(TRANSLATE(replace(replace(A, ' '), ',', '.'), '0123456789.-', ' ')),
null,
DECODE(INSTR(replace(replace(A, ' '), ',', '.'), '.', INSTR(replace(replace(A, ' '), ',', '.'), '.') + 1),
0,
DECODE(INSTR(replace(replace(A, ' '), ',', '.'), '-', 2),
0,
TO_NUMBER(replace(replace(A, ' '), ',', '.'))))) A
from (select '-1.1' A from DUAL union all select '-1-1' A from DUAL union all select ',1' A from DUAL union all select '1..1' A from DUAL) A;
This code excludes such strings as: -1-1, 1..1, 12-2 and so on. And I haven't used regular expressions here.
A combination of previous solutions (from #sOliver and #Mike Meyers) and trying to grab as much numbers as possible by removing the last '$' from REGEXP.
It can be used to filter the actual number from a configuration table, and have a "kind-of" comment next to the number as '12 Days'.
with my_sample_data as (
select '12345' as mynum from dual union all
select '123.4567' as mynum from dual union all
select '-0.3462' as mynum from dual union all
select '.34567' as mynum from dual union all
select '-.1234' as mynum from dual union all
select '**' as mynum from dual union all
select '0.34.62' as mynum from dual union all
select '24Days' as mynum from dual union all
select '42ab' as mynum from dual union all
select '54-3' as mynum from dual
)
SELECT mynum,
COALESCE( TO_NUMBER( REGEXP_SUBSTR( mynum, '^(-|+)?\d*(.|,)?(\d+)?') ) , 0) is_num
FROM my_sample_data;
would give
MYNUM IS_NUM
-------- ----------
12345 12345
123.4567 123.4567
-0.3462 -0.3462
.34567 0.34567
-.1234 -0.1234
** 0
0.34.62 0.34
24Days 24
42ab 42
54-3 54
Best method seems to be the function solution but if you don't have necessary privileges in the environment you are struggling (like me), then you can try this one:
SELECT
CASE
WHEN
INSTR(TRANSLATE('123O0',
' qwertyuıopğüasdfghjklşizxcvbnmöçQWERTYUIOPĞÜASDFGHJKLŞİZXCVBNMÖÇ~*\/(){}&%^#$<>;#€|:_=',
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
),
'X') > 0
THEN 'Y'
ELSE 'N'
END is_nonnumeric
FROM DUAL
By the way: In my case the problem was due to "," and "." :) So take that into consider. Inspired from this one. Also this one seems more concise.
By the way 2: Dear Oracle, can you please create some built-in functions for such small but invaluable needs?

Resources