-2

I have this alphanumeric string

000193CA020008AA190038AA

I want it to be a unique number, for example

123456791011121314151617.

Is there a way to do it on PLSQL? Not all aphanumerics are the same length, does not contain hex numbers only.

I have tried

create or replace FUNCTION FN_BOM_STEEL_CHAR_TO_NUMBER(
  value IN VARCHAR2
) RETURN NUMBER DETERMINISTIC
IS
  c        CHAR(1);
  up_value VARCHAR2(5000) := LTRIM(UPPER(value),'0');
  total    NUMBER  := 0;
  len      PLS_INTEGER    := COALESCE(LENGTH(up_value),0);
BEGIN
  FOR i IN 1 .. len LOOP
    c := SUBSTR(up_value,i,1);
    total := total * 36
                   + CASE
                     WHEN '0' <= c AND c <= '9'
                     THEN TO_NUMBER(c)
                     ELSE ASCII(c) - 55
                     END;
  END LOOP;
  RETURN total;
END FN_BOM_STEEL_CHAR_TO_NUMBER;

and it worked really well but I have just found there are some duplicates with different alphanumeric strings, for example

  1. 010000AA000001AA020019AA020047AA020138AA020139AA060018AA190016AA

  2. 010000AA000001AA020019AA020047AA020138AA020139AA060019AA190016AA

You can see there is a 18 and 19 that makes the difference on them but when I select the function I'm getting the same ID for both. There are more cases like these.

Here's the select I'm trying for those examples

SELECT FN_BOM_STEEL_CHAR_TO_NUMBER('010000AA000001AA020019AA020047AA020138AA020139AA060018AA190016AA
') AS ID
FROM   DUAL;

and this is the ID for both: 3095673299759705115226102843045930360610000000000000000000000000000000000000000000000000000000000

I need them to be different. Thanks in advance!

Ren Cantu
  • 67
  • 2
  • 10
  • 3
    Is it possible? Probably, if you tell us the algorithm which converts "source" to "target". – Littlefoot Jan 06 '21 at 20:39
  • 2
    Can you show us some sample data with output, and the logic behind the said conversion? – Radagast Jan 06 '21 at 20:47
  • 1
    Uniqueness depends on many conditions. Should it be unique during some time frame or for all the database's life? Should the same source string (or parts of it) be translated to the same result string (which is known as function)? Do source and target need to have any relation between them or result number may be any number? – astentx Jan 06 '21 at 20:50

1 Answers1

0

If you just have the characters 0123456789ABCDEF then you can use:

SELECT TO_NUMBER(
         '000193CA020008AA190038AA',
         'XXXXXXXXXXXXXXXXXXXXXXXX'
       ) AS from_hex_value
FROM   DUAL;

Which outputs:

|            FROM_HEX_VALUE |
| ------------------------: |
| 1906840079024071083309226 |

If you have the characters 0-9A-Z and want to treat it as a base 36 string then you could create the function:

CREATE FUNCTION base36_to_number(
  value  IN VARCHAR2,
  -- The value to parse from base 36 to base 10.
  width  IN INT DEFAULT 0,
  -- The width of the substring to consider; 0 is the entire string.
  offset IN INT DEFAULT 0
  -- The number of widths to offset from the least-significant digit.
) RETURN NUMBER DETERMINISTIC
IS
  c         CHAR(1);
  total     NUMBER := 0;
  len       PLS_INTEGER    := COALESCE(LENGTH(value),0);
  start_pos PLS_INTEGER;
  end_pos   PLS_INTEGER;
BEGIN
  IF width < 0 THEN
    RAISE_APPLICATION_ERROR( -20000, 'Invalid width.' );
  ELSIF offset < 0 THEN
    RAISE_APPLICATION_ERROR( -20000, 'Invalid offset.' );
  ELSIF width = 0 THEN
    start_pos := 1;
    end_pos   := len;
  ELSIF width * offset > len THEN
    RETURN 0;
  ELSIF width * (offset + 1) > len THEN
    start_pos := 1;
    end_pos   := len - width  * offset;
  ELSE
    start_pos := len - width * ( offset + 1 ) + 1;
    end_pos   := start_pos + width - 1;
  END IF;

  FOR i IN start_pos .. end_pos LOOP
    c := SUBSTR(value,i,1);
    total := total * 36
                   + CASE
                     WHEN '0' <= c AND c <= '9'
                     THEN TO_NUMBER(c)
                     WHEN 'A' <= c AND c <= 'Z'
                     THEN ASCII(c) - 55
                     WHEN 'a' <= c AND c <= 'z'
                     THEN ASCII(c) - 87
                     ELSE NULL
                     END;
  END LOOP;
  
  total := total * POWER( 36, width * offset );
  RETURN total;
END;
/

Then:

SELECT BASE36_TO_NUMBER('000193CA020008AA190038AA') AS from_base36_value
FROM   DUAL;

Outputs:

|                FROM_BASE36_VALUE |
| -------------------------------: |
| 16743829188442650222733747166386 |

The maximum size of a NUMBER is 38 decimal digits and you cannot fit more than a 24 character base-36 number into this. If you have a larger value then you will need to convert it to multiple numbers:

I.e.:

CREATE TABLE table_name ( value ) AS
SELECT '010000AA000001AA020019AA020047AA020138AA020139AA060018AA190016AA' FROM DUAL UNION ALL
SELECT '010000AA000001AA020019AA020047AA020138AA020139AA060019AA190016AA' FROM DUAL UNION ALL
SELECT 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZzzzzzzzzzzzzzzzzzzzzzzzz' FROM DUAL;

Then:

SELECT BASE36_TO_NUMBER( value, 24, 0 ) AS low_value,
       BASE36_TO_NUMBER( value, 24, 1 ) AS mid_value,
       BASE36_TO_NUMBER( value, 24, 2 ) AS high_value
FROM   table_name;

Outputs:

                             LOW_VALUE |                                                                   MID_VALUE |                                                                                           HIGH_VALUE
-------------------------------------: | --------------------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------:
   34663123071664682325935025837090322 |    777948567801018784511679412336101916888000000000000000000000000000000000 |    3095673299759705115226102065097362559570000000000000000000000000000000000000000000000000000000000
   34663123071664682329591184277153298 |    777948567801018784511679412336101916888000000000000000000000000000000000 |    3095673299759705115226102065097362559570000000000000000000000000000000000000000000000000000000000
22452257707354557240087211123792674815 | 504103876157462118901767181449118688664000000000000000000000000000000000000 | 4011991914547630480065052883598567655228000000000000000000000000000000000000000000000000000000000000

db<>fiddle here

MT0
  • 143,790
  • 11
  • 59
  • 117
  • what if not all alphanumerics are the same length because tried and it obviously didn't work out SELECT TO_NUMBER(NUM_SEPARADOR, 'XXXXXXXXXXXXXXXXXXXXXXXX' ) AS ID_SEPARADOR FROM tmp_bom_steel7; I also tried SELECT TO_NUMBER(NUM_SEPARADOR) AS ID_SEPARADOR FROM tmp_bom_steel7; but keeps telling me wrong invalid number. I'm a beginner, sorry for the simple questions – Ren Cantu Jan 06 '21 at 21:49
  • @RenataEsquivel If your column is a `VARCHAR2(50)` then use 50 `X` characters so that the format model is as long as the largest possible string value. – MT0 Jan 06 '21 at 22:25
  • Also, if your column does not contain hex numbers only (this is digits and A-F letters only), you could make use of `dbms_crypto` and generate sha1 first. Sha1 is 40 characters long. – Krzysztof Kaszkowiak Jan 06 '21 at 22:36
  • @KrzysztofKaszkowiak It is theoretically possible to have hash collisions (and they have been generated) so SHA1 may not meet the question's requirement for uniqueness (practically, you aren't likely to encounter a hash collision so that may be sufficient). – MT0 Jan 06 '21 at 23:39
  • does not contain hex numbers only that's the reason of the error. – Ren Cantu Jan 06 '21 at 23:54
  • Thank you so much it worked @MT0 today learned something new. Greetings! – Ren Cantu Jan 07 '21 at 01:38
  • @MT0 I just edited the post I hope you can see what is happening now. Thanks! – Ren Cantu Jan 07 '21 at 07:37
  • @RenataEsquivel That is because your values are too big and, in Oracle, a `NUMBER` can only store up to 38 decimal digits and if you exceed that then precision will be lost. You need to split the string up into smaller sub-strings. – MT0 Jan 07 '21 at 10:43