Oracle to_char format number with fill mode (FM0000) - sql

I use the TO_CHAR function to format number from 0001 to 9999, and to fit the column size (VARCHAR2(4)) where the value is inserted (even if value is > 9999).
I use the function like this:
TO_CHAR(n, 'FM0000')
Examples that work:
SELECT TO_CHAR(1, 'FM0000') FROM DUAL;
Result: 0001
SELECT TO_CHAR(1234, 'FM0000') FROM DUAL;
Result: 1234
But when I test with a value greater than 9999, I get an extra character:
SELECT TO_CHAR(12345, 'FM0000') FROM DUAL;
Result: #####
SELECT TO_CHAR(123456, 'FM0000') FROM DUAL;
Result: #####
For information, the result I expected was #### (on 4 chars).
To sum up:
When the value to convert corresponds to the expected size (4), the converted value has the same length (4)
When the value to convert is longer than the expected size (5 or more), the converted value has one more character than the expected length (5).
How to explain this ?
I didn't found explanation in the Oracle documentation here https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements004.htm#i170559
I tried on several Oracle version (9, 10, 11) and the result is the same.
The workaround I found is to truncate the result with RPAD() function RPAD(TO_CHAR(n,'FM0000'), 4) but I need to understand why the TO_CHAR function is not enough.

Your format model still has to allow for the sign of the value. There is no way to indicate to TO_CHAR() that it can never be negative (if that is in fact the case for your values). Even with a 4-digit number the formatting allows allows for five characters, as you can see from the column heading:
SQL> SELECT TO_CHAR(1234, 'FM0000') FROM DUAL;
TO_CH
-----
1234
Notice the column heading is TO_CH, which is five characters, not four. If you have a negative number (as Florin suggested) you need that extra space:
SQL> SELECT TO_CHAR(-1234, 'FM0000') FROM DUAL;
TO_CH
-----
-1234
Without the FM modifier you get a leading space in the returned string for positive values, so LENGTH(TO_CHAR(1234, '0000')) is 5 but LENGTH(TO_CHAR(1234, 'FM0000')) is 4, because the leading space (which normally makes the values in the column right-justified) is suppressed. With a negative value the length of the returned string is 5 either way. The format model determines that the returned data type is varchar2(5) to allow for the sign, even if you know there will never be negative values - there isn't any way for the format model to reflect that.
You can see it with positive values too if you force the sign to be shown:
SQL> SELECT TO_CHAR(1234, 'FMS0000') FROM DUAL;
TO_CH
-----
+1234
There isn't anything you can do about that in the TO_CHAR call. As an alternative to your RPAD workaround, you could use SUBSTR to only get the last four characters of the formatted string:
SQL> SELECT SUBSTR(TO_CHAR(12345, 'FM0000'), -4) FROM DUAL
SUBSTR(TO_CHAR(1
----------------
####
But if you do have negative values you'd lose the sign:
SQL> SELECT SUBSTR(TO_CHAR(-1234, 'FM0000'), -4) FROM DUAL
SUBSTR(TO_CHAR(-
----------------
1234
With your RPAD you keep the sign but lose the fourth significant digit:
SQL> SELECT RPAD(TO_CHAR(-1234, 'FM0000'), 4) FROM DUAL
RPAD(TO_CHAR(-12
----------------
-123
which also isn't good. You may not have to deal with negative numbers; but if you're dealing with number larger than you expect (i.e. you get a number >= 10000 when you're expecting only <= 9999) then I'm not sure you can be certain you won't see an (invalid?) negative number at some point too. This seems to be a data problem rather than a formatting problem, on some level anyway.
Based on your comment to Ollie, another approach which might be more explicit and obvious to future maintainers of the code is to spell it out in a CASE:
SELECT CASE WHEN n BETWEEN 0 AND 9999 THEN TO_CHAR(n, 'FM0000') ELSE '####' END FROM DUAL
Which would also allow you to leave the string column null or use some other magic value rather than ####, if you wanted to.
And another way to trim the value, which may also be clearer, is with CAST:
SQL> SELECT CAST(TO_CHAR(12345, 'FM0000') AS VARCHAR2(4)) FROM DUAL;
CAST
----
####

Related

Postgresql decimal point using to_char

I want make a select that adds decimal point into integers, but when I do it, it shows me bunch of # instead of those numbers.
SELECT to_char(1234, '99.99');
What I expected was table with a value of 12.34 but I got ##.## in my select.
However, if I did
SELECT to_char(1234, '99,99');
it showed be 12,34 as expected. Problem is, that I want to have a decimal point and not a comma.
I am using PostgreSQL 13.2
It seems you want to take the last 2 digits and pretend they were decimals. You can't use the predefined . or D formats because they apply to true decimals.
Instead, you can print the dot character (like any other string), between double quotes, before the last 2 digits:
SELECT to_char(1234, '999"."99');
to_char
---------
12.34
PS: on a side note, you are getting the masked output in your 1st query because there isn't enough digit positions on the format:
SELECT to_char(1234, '9999.99');
to_char
----------
1234.00
Welcome to SO. You were very close:) Which locale are you using? Check this example for en_US.UTF-8:
SELECT to_char(1234, '999G99');
to_char
---------
12.34
(1 row)
G: Group separator that uses locale
D: Decimal point that uses locale
Check this tutorial

Leading Zero is getting truncated in Oracle SQL query while doing currency format

Query:
select to_char('0.00', '$999,999,999,999,999.99') from dual;
Actual output:
$.00
Expected Output:
$0.0
Is this what you want?
select to_char('0.00', '$999,999,999,999,990.9') from dual;
The 0 on the first digit forces Oracle to display something, even if the digit is not significant. I also changed the specifier to have just one decimal number instead of two .

TO_Char number format model in Oracle

I don't understand fully how can I use the to_char function to convert a number to a string with the appropriate format model.
The actual number has this type of format:
Uses comma as decimal separator
Always 5 decimal numbers
The integer numbers can up to 6 (potentially can be infinite, but for now they were never more than 6)
The number can be positive or negative
The number can begin with a 0
I've tried to use the to_char but I am not able to achieve a result that works with all the conditions
Some examples of how the output should be are
0,00235 or 156,45623 or -0,0235 or -156,45623
Keep in mind that you are transforming a number into a string. The number does not have any sense of "," or "." or anything--it is a number.
The trick is to get the TO_CHAR function to convert the internal number to the string representation that you want.
There are a few problems to worry about: getting the radix point (decimal) correct and dealing with padding.
Here is a working example:
SELECT to_char(0.00235,'FM99999999999999990D99999', 'NLS_NUMERIC_CHARACTERS = '',.''') FROM DUAL;
0,00235
SELECT to_char(156.45823,'FM99999999999999990D99999', 'NLS_NUMERIC_CHARACTERS = '',.''') FROM DUAL;
156,45823
SELECT to_char(-0.0235,'FM99999999999999990D99999', 'NLS_NUMERIC_CHARACTERS = '',.''') FROM DUAL;
-0,0235
SELECT to_char(-156.45623,'FM99999999999999990D99999', 'NLS_NUMERIC_CHARACTERS = '',.''') FROM DUAL;
-156,45623
SELECT to_char(123456789.45623,'FM99999999999999990D99999', 'NLS_NUMERIC_CHARACTERS = '',.''') FROM DUAL;
123456789,45623
The relevant parts of the mask:
FMis used to trim leading and trailing blanks that Oracle normally uses to pad out numbers.
D is the radix point, depending on your NLS settings.
NLS_NUMERIC_CHARACTERS ... is an override of your local NLS settings--this might not be necessary if your locale uses a comma for the decimal, but it is a way you can force this behavior in a database with, say, North American settings.
Shamelessly stolen from this post from #Vadzim.
You should be able to get the format you're looking for by using this pattern:
rtrim(to_char(num, 'FM999999999999990.99'), '.')
https://rextester.com/QRSD48676
SELECT rtrim(to_char('0,00235', 'FM999999999999990.99999'), '.') FROM DUAL\\
SELECT rtrim(to_char('156,45623', 'FM999999999999990.99999'), '.') FROM DUAL\\
SELECT rtrim(to_char('-0,0235', 'FM999999999999990.99999'), '.') FROM DUAL\\
SELECT rtrim(to_char('-156,45623', 'FM999999999999990.99999'), '.') FROM DUAL\\
Results:
0.00235
156.45623
-0.0235
-156.45623

Adding decimal point and zeroes in Oracle

Is there a way to specify that a double datatype will always have 2 points after the decimal point like currency usually has (oracle db) ?
that is:
100 will be converted to 100.00
101.1 will be converted to 101.10
Tried casting it and setting precision-scale but no luck with that it doesnt add default decimal point and zeroes
SELECT CAST((600.2) AS NUMERIC(28,2)) FROM DUAL;
Gives me 600.2
SELECT to_char(101, '999.90') FROM DUAL;
SELECT to_char(101.1, '999.90') FROM DUAL;
It is just formatting the output in your client.
For example, in SQL*Plus set numformat:
SQL> set numformat 999.99
SQL> SELECT CAST((600.2) AS NUMERIC(28,2)) FROM DUAL;
CAST((600.2)ASNUMERIC(28,2))
----------------------------
600.20
You could also use TO_CHAR, but use it only to display, for any number arithmetic you should leave the number as it is.
SQL> select to_char(600.2, '000.00') from dual;
TO_CHAR
-------
600.20

Why are these NVL() statements wrong in SQL?

SELECT inv_no,NVL2(inv_amt,inv_date,'Not Available')
FROM invoice;
SELECT inv_no,NVL2(inv_amt,inv_amt*.25,'Not Available') FROM invoice;
getting
ORA-01858: a non-numeric character was found where a numeric was expected
ORA-01722: invalid number
respectively
Please suggest what are the datatypes I can give in expr1 and expr2.
Also Please tell me how this is right?
SELECT inv_no,NVL2(inv_date,sysdate-inv_date,sysdate)
FROM invoice
The 2nd and 3rd parameters to NVL2 should be the same datatype. So, assuming inv_date is a DATE, you'd need to have that as a varchar2 like;
SELECT inv_no, NVL2(inv_amt, to_char(inv_date, 'dd-mon-yyyy hh24:mi:ss'), 'Not Available') FROM ...
or whatever format string you wanted. Otherwise Oracle will convert the 3rd parameter 'Not Available' to match the 2nd parameter's data type. This will try to convert 'Not Available' to a date and crash.
Similarly in the 2nd example, you have to convert the inv_amt*.25 to a char viato_char, e.g. to_char(inv_amt*.25).
Your first 2 examples attempt to have a date / numeric and text results for the same field. This will cause an error when Oracle attempts to convert this text to either types. You'll need to use the to_char(field) function on the date / numeric fields to convert them to text.
Lastly a date is in fact a number added to a databases base date. For example a date is a number of base day and the decimal it has is the ratio of a day, e.g. 0.5 is 12 hours, and the database has a base date, e.g. 01-Jan-1900 or 01-Jan-2000. This is why when you do date - date the result is a number and a date can also be represented as a number.
Refer to: https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions106.htm
The 2nd argument should be either character or numeric. The 2nd argument gives datatype to which the 3rd argument will be implictly converted. So:
1. The first one SELECT inv_no,NVL2(inv_amt,inv_date,'Not Available') FROM invoice; is incorrect as you cannot have DATE as a 2nd argument.
The same way:
SELECT NVL2(NULLIF(1, 1), SYSDATE, 'A') FROM DUAL; -- FAILS;
SELECT NVL2(NULLIF(1, 0), SYSDATE, 'A') FROM DUAL; -- FAILS;
SELECT NVL2(NULLIF(1, 1), 'A', SYSDATE) FROM DUAL; -- PASS; returns SYSDATE (as char datatype)
SELECT NVL2(NULLIF(1, 0), 'A', SYSDATE) FROM DUAL; -- PASS; returns 'A';
2. The second is invalid too: SELECT inv_no,NVL2(inv_amt,inv_amt*.25,'Not Available') FROM invoice; Oracle tries to implicitly convert 3rd argument to the datatype of the 2nd arugment, meaning it tries to convert 'Not available' to numeric value.
It will work when you swap 2nd and 3rd argument:
SELECT inv_no,NVL2(inv_amt,'Not Available',inv_amt*.25) FROM invoice;
inv_amt*.25 will be implicitly converted to character data; This example is useless as it will get you NULL*.25 making it NULL.
What you can do however is you can make both parameters as characters by converting numeric result to character:
SELECT inv_no,NVL2(inv_amt,TO_CHAR(inv_amt*.25), 'Not Available') FROM invoice;
It will make some sense particularly when you want to display currency.
3. The third one is more complex
The only way I can explain is that the 2nd argument gets converted to character before dealing with the third one. That way the third one will get converted to characters too. This would make sense given the two cases:
SELECT NVL2(2, SYSDATE - TO_DATE('10-JAN-83'), SYSDATE) from dual; -- PASSES
SELECT NVL2(2, 2.2, SYSDATE) from dual; -- FAILS.
Finally: I would advise to stick to Oracle's documentation and use explicit conversion.