Convert hex in text representation to decimal number
Asked Answered
T

10

49

I am trying to convert hex to decimal using PostgreSQL 9.1

with this query:

SELECT to_number('DEADBEEF', 'FMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');

I get the following error:

ERROR:  invalid input syntax for type numeric: " "

What am I doing wrong?

Typhogenic answered 29/11, 2011 at 18:55 Comment(1)
can you revaluate this with this moder modern answer https://mcmap.net/q/28837/-convert-hex-in-text-representation-to-decimal-numberTobit
C
30

You have two immediate problems:

  1. to_number doesn't understand hexadecimal.
  2. X doesn't have any meaning in a to_number format string and anything without a meaning apparently means "skip a character".

I don't have an authoritative justification for (2), just empirical evidence:

=> SELECT to_number('123', 'X999');
 to_number 
-----------
        23
(1 row)

=> SELECT to_number('123', 'XX999');
 to_number 
-----------
         3

The documentation mentions how double quoted patterns are supposed to behave:

In to_date, to_number, and to_timestamp, double-quoted strings skip the number of input characters contained in the string, e.g. "XX" skips two input characters.

but the behavior of non-quoted characters that are not formatting characters appears to be unspecified.

In any case, to_number isn't the right tool for converting hex to numbers, you want to say something like this:

select x'deadbeef'::int;

so perhaps this function will work better for you:

CREATE OR REPLACE FUNCTION hex_to_int(hexval varchar) RETURNS integer AS $$
DECLARE
    result  int;
BEGIN
    EXECUTE 'SELECT x' || quote_literal(hexval) || '::int' INTO result;
    RETURN result;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

Then:

=> select hex_to_int('DEADBEEF');
 hex_to_int 
------------
 -559038737 **
(1 row)

** To avoid negative numbers like this from integer overflow error, use bigint instead of int to accommodate larger hex numbers (like IP addresses).

Complainant answered 29/11, 2011 at 19:44 Comment(1)
Haha, odd, to_number supports the silliest things such as Roman numerals, ordinal suffixes and whatnot -- when's the last time anyone needed that. :) But no hex?!Lenardlenci
D
103

Ways without dynamic SQL

There is no cast from hex numbers in text representation to a numeric type, but we can use bit(n) as waypoint. There are undocumented casts from bit strings (bit(n)) to integer types (int2, int4, int8) - the internal representation is binary compatible. Quoting Tom Lane:

This is relying on some undocumented behavior of the bit-type input converter, but I see no reason to expect that would break. A possibly bigger issue is that it requires PG >= 8.3 since there wasn't a text to bit cast before that.

integer for max. 8 hex digits

Up to 8 hex digits can be converted to bit(32) and then coerced to integer (standard 4-byte integer):

SELECT ('x' || lpad(hex, 8, '0'))::bit(32)::int AS int_val
FROM  (
   VALUES
      ('1'::text)
    , ('f')
    , ('100')
    , ('7fffffff')
    , ('80000000')     -- overflow into negative number
    , ('deadbeef')
    , ('ffffffff')
    , ('ffffffff123')  -- too long
   ) AS t(hex);
   int_val
------------
          1
         15
        256
 2147483647
-2147483648
 -559038737
         -1

Postgres uses a signed integer type, so hex numbers above '7fffffff' overflow into negative integer numbers. This is still a valid, unique representation but the meaning is different. If that matters, switch to bigint; see below.

For more than 8 hex digits the least significant characters (excess to the right) get truncated.

4 bits in a bit string encode 1 hex digit. Hex numbers of known length can be cast to the respective bit(n) directly. Alternatively, pad hex numbers of unknown length with leading zeros (0) as demonstrated and cast to bit(32). Example with 7 hex digits and int or 8 digits and bigint:

SELECT ('x'|| 'deafbee')::bit(28)::int
     , ('x'|| 'deadbeef')::bit(32)::bigint;
  int4     | int8
-----------+------------
 233503726 | 3735928559

bigint for max. 16 hex digits

Up to 16 hex digits can be converted to bit(64) and then coerced to bigint (int8, 8-byte integer) - overflowing into negative numbers in the upper half again:

SELECT ('x' || lpad(hex, 16, '0'))::bit(64)::bigint AS int8_val
FROM  (
   VALUES
      ('ff'::text)
    , ('7fffffff')
    , ('80000000')
    , ('deadbeef')
    , ('7fffffffffffffff')
    , ('8000000000000000')     -- overflow into negative number
    , ('ffffffffffffffff')
    , ('ffffffffffffffff123')  -- too long
   ) t(hex);
       int8_val
---------------------
                 255
          2147483647
          2147483648
          3735928559
 9223372036854775807
-9223372036854775808
                  -1
                  -1

Inverse

To convert back either integer or bigint, use the built-in (overloaded) function to_hex():

SELECT to_hex(3735928559);  -- → 'deadbeef'

uuid for max. 32 hex digits

The Postgres uuid data type is not a numeric type. But it's the most efficient type in standard Postgres to store up to 32 hex digits, only occupying 16 bytes of storage. There is a direct cast from text to uuid (no need for bit(n) as waypoint), but exactly 32 hex digits are required.

SELECT lpad(hex, 32, '0')::uuid AS uuid_val
FROM  (
   VALUES ('ff'::text)
        , ('deadbeef')
        , ('ffffffffffffffff')
        , ('ffffffffffffffffffffffffffffffff')
        , ('ffffffffffffffffffffffffffffffff123') -- too long
   ) t(hex);
              uuid_val
--------------------------------------
 00000000-0000-0000-0000-0000000000ff
 00000000-0000-0000-0000-0000deadbeef
 00000000-0000-0000-ffff-ffffffffffff
 ffffffff-ffff-ffff-ffff-ffffffffffff
 ffffffff-ffff-ffff-ffff-ffffffffffff

As you can see, standard output is a string of hex digits with typical separators for UUID.

md5 hash

This is particularly useful to store md5 hashes:

SELECT md5('Store hash for long string, maybe for index?')::uuid AS md5_hash;
           md5_hash
--------------------------------------
 02e10e94-e895-616e-8e23-bb7f8025da42

See:

Dick answered 1/12, 2011 at 1:11 Comment(1)
Another method of doing it with pg-bignum https://mcmap.net/q/28837/-convert-hex-in-text-representation-to-decimal-numberTobit
C
30

You have two immediate problems:

  1. to_number doesn't understand hexadecimal.
  2. X doesn't have any meaning in a to_number format string and anything without a meaning apparently means "skip a character".

I don't have an authoritative justification for (2), just empirical evidence:

=> SELECT to_number('123', 'X999');
 to_number 
-----------
        23
(1 row)

=> SELECT to_number('123', 'XX999');
 to_number 
-----------
         3

The documentation mentions how double quoted patterns are supposed to behave:

In to_date, to_number, and to_timestamp, double-quoted strings skip the number of input characters contained in the string, e.g. "XX" skips two input characters.

but the behavior of non-quoted characters that are not formatting characters appears to be unspecified.

In any case, to_number isn't the right tool for converting hex to numbers, you want to say something like this:

select x'deadbeef'::int;

so perhaps this function will work better for you:

CREATE OR REPLACE FUNCTION hex_to_int(hexval varchar) RETURNS integer AS $$
DECLARE
    result  int;
BEGIN
    EXECUTE 'SELECT x' || quote_literal(hexval) || '::int' INTO result;
    RETURN result;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

Then:

=> select hex_to_int('DEADBEEF');
 hex_to_int 
------------
 -559038737 **
(1 row)

** To avoid negative numbers like this from integer overflow error, use bigint instead of int to accommodate larger hex numbers (like IP addresses).

Complainant answered 29/11, 2011 at 19:44 Comment(1)
Haha, odd, to_number supports the silliest things such as Roman numerals, ordinal suffixes and whatnot -- when's the last time anyone needed that. :) But no hex?!Lenardlenci
T
8

pg-bignum

Internally, pg-bignum uses the SSL library for big numbers. This method has none of the drawbacks mentioned in the other answers with numeric. Nor is it slowed down by plpgsql. It's fast and it works with a number of any size. Test case taken from Erwin's answer for comparison,

CREATE EXTENSION bignum;

SELECT hex, bn_in_hex(hex::cstring) 
FROM   (
   VALUES ('ff'::text)
        , ('7fffffff')
        , ('80000000')
        , ('deadbeef')
        , ('7fffffffffffffff')
        , ('8000000000000000')
        , ('ffffffffffffffff')
        , ('ffffffffffffffff123')
   ) t(hex);

         hex         |        bn_in_hex        
---------------------+-------------------------
 ff                  | 255
 7fffffff            | 2147483647
 80000000            | 2147483648
 deadbeef            | 3735928559
 7fffffffffffffff    | 9223372036854775807
 8000000000000000    | 9223372036854775808
 ffffffffffffffff    | 18446744073709551615
 ffffffffffffffff123 | 75557863725914323415331
(8 rows)

You can get the type to numeric using bn_in_hex('deadbeef')::text::numeric.

Tobit answered 9/12, 2017 at 1:39 Comment(2)
Interesting. It's a pity that most hosted DBs only allow a limited list of approved extensions.Dick
Unfortunately, this solution suffers from not being in the official packages (available as described in wiki.postgresql.org/wiki/Apt) which I find a little risky in terms of reliability, x-platform installability and so on. It is also not very documented. I think bignum really belongs into core!Comfort
U
8

Here is a version which uses numeric, so it can handle arbitrarily large hex strings:

create function hex_to_decimal(hex_string text)
returns text
language plpgsql immutable as $pgsql$
declare
    bits bit varying;
    result numeric := 0;
    exponent numeric := 0;
    chunk_size integer := 31;
    start integer;
begin
    execute 'SELECT x' || quote_literal(hex_string) INTO bits;
    while length(bits) > 0 loop
        start := greatest(1, length(bits) - chunk_size);
        result := result + (substring(bits from start for chunk_size)::bigint)::numeric * pow(2::numeric, exponent);
        exponent := exponent + chunk_size;
        bits := substring(bits from 1 for greatest(0, length(bits) - chunk_size));
    end loop;
    return trunc(result, 0);
end
$pgsql$;

For example:

=# select hex_to_decimal('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
32592575621351777380295131014550050576823494298654980010178247189670100796213387298934358015
Uprising answered 18/10, 2018 at 22:33 Comment(3)
Not work for '5f68e8131ecf80000'.Zwinglian
There is some bug in the function causing it to return wrong output for some input. Example: SELECT hex_to_decimal('ffffffff00000001000000000000000000000000ffffffffffffffffffffffff'); -- Returned: 115792089223836222093134215979047740691678064040465439117495607658390113746943 -- Expected: 115792089210356248762697446949407573530086143415290314195533631308867097853951Lambeth
I corrected the function, see the post below. Now it works correctly.Diphase
A
5

If anybody else is stuck with PG8.2, here is another way to do it.

bigint version:

create or replace function hex_to_bigint(hexval text) returns bigint as $$
select
  (get_byte(x,0)::int8<<(7*8)) |
  (get_byte(x,1)::int8<<(6*8)) |
  (get_byte(x,2)::int8<<(5*8)) |
  (get_byte(x,3)::int8<<(4*8)) |
  (get_byte(x,4)::int8<<(3*8)) |
  (get_byte(x,5)::int8<<(2*8)) |
  (get_byte(x,6)::int8<<(1*8)) |
  (get_byte(x,7)::int8)
from (
  select decode(lpad($1, 16, '0'), 'hex') as x
) as a;
$$
language sql strict immutable;

int version:

create or replace function hex_to_int(hexval text) returns int as $$
select
  (get_byte(x,0)::int<<(3*8)) |
  (get_byte(x,1)::int<<(2*8)) |
  (get_byte(x,2)::int<<(1*8)) |
  (get_byte(x,3)::int)
from (
  select decode(lpad($1, 8, '0'), 'hex') as x
) as a;
$$
language sql strict immutable;
Announce answered 11/7, 2014 at 21:51 Comment(0)
N
1

Here is another implementation:

CREATE OR REPLACE FUNCTION hex_to_decimal3(hex_string text)
 RETURNS numeric
 LANGUAGE plpgsql
 IMMUTABLE
AS $function$
declare
    hex_string_lower text := lower(hex_string);
    i int;
    digit int;
    s numeric := 0;
begin
    for i in 1 .. length(hex_string) loop
        digit := position(substr(hex_string_lower, i, 1) in '0123456789abcdef') - 1;
        if digit < 0 then
            raise '"%" is not a valid hexadecimal digit', substr(hex_string_lower, i, 1) using errcode = '22P02'; 
        end if;
        s := s * 16 + digit;
    end loop;
   
    return s;
end
$function$;

It is a straightforward one that works digit by digit, using the position() function to compute the numeric value of each character in the input string. Its benefit over hex_to_decimal2() is that it seems to be much faster (4x or so for md5()-generated hex strings).

Nonary answered 28/12, 2022 at 16:34 Comment(0)
D
1

David Wolever's function hex_to_Decimal has a mistake. Here is the fixed version of the function:

create or replace function ${migrSchemaName4G}.hex_to_decimal(hex_string text)
    returns numeric
    language plpgsql immutable as $pgsql$
declare
    bits bit varying;
    result numeric := 0;
    exponent numeric := 0;
    chunk_size integer := 31;
    start integer;
begin
    execute 'SELECT x' || quote_literal(hex_string) INTO bits;
    while length(bits) > 0 loop
            start := greatest(0, length(bits) - chunk_size) + 1;
            result := result + (substring(bits from start for chunk_size)::bigint)::numeric * pow(2::numeric, exponent);
            exponent := exponent + chunk_size;
            bits := substring(bits from 1 for greatest(0, length(bits) - chunk_size));
        end loop;
    return result;
end
$pgsql$;

There are many examples of the HEX translation into Number here, but most of them have a restriction on the length of HEX. I needed to solve the problem of a uniform distribution of identical records in different databases (Oracle and Postgres) on the "baskets". In this case, the same records in different databases should fall into the same "baskets" of processing. The identifier of the records is string.

Oracle has an ORA_HASH function. But there is no such function in Postgres. The problem was solved by using the same md5 caching functions, which generates a 32-symbol hex long. Only the above function helped, thanks to David Wolever.

For Oracle, the filtering condition turned out to be: MOD(TO_NUMBER(standard_hash(ID, 'MD5'), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'), ${processCount}) = ${processNumber}

For Postgres - hex_to_Decimal(MD5(ID)) % ${Processcount} = ${Processnumber}

Diphase answered 5/6, 2023 at 13:6 Comment(0)
O
0

Here is a poper way to convert hex to string... then you can check whether it's a numric type or not

SELECT convert_from('\x7468697320697320612076657279206C6F6E672068657820737472696E67','utf8')

returns

this is a very long hex string
Oleta answered 2/2, 2022 at 23:14 Comment(0)
Z
0

Here is a other version which uses numeric, so it can handle arbitrarily large hex strings:

create OR REPLACE function hex_to_decimal2(hex_string text)
returns text
language plpgsql immutable as $pgsql$
declare
    bits bit varying;
    result numeric := 0;
begin
    execute 'SELECT x' || quote_literal(hex_string) INTO bits;
    while length(bits) > 0 loop
        result := result + (substring(bits from 1 for 1)::bigint)::numeric * pow(2::numeric, length(bits) - 1);
    bits := substring(bits from 2 for length(bits) - 1);
    end loop;
    return trunc(result, 0);
end
$pgsql$;

For example:

=# select hex_to_decimal('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
32592575621351777380295131014550050576823494298654980010178247189670100796213387298934358015

For example:

=# select hex_to_decimal('5f68e8131ecf80000');
110000000000000000000
Zwinglian answered 19/9, 2022 at 5:34 Comment(0)
L
0
CREATE OR REPLACE FUNCTION numeric_from_bytes(bytea)
RETURNS numeric
LANGUAGE plpgsql
AS $$
declare
  bits bit varying;
  result numeric := 0;
  exponent numeric := 0;
  bit_pos integer;
begin
  execute 'SELECT x' || quote_literal(substr($1::text,3)) into bits;
  bit_pos := length(bits) + 1;
  exponent := 0;
  while bit_pos >= 56 loop
    bit_pos := bit_pos - 56;
    result := result + substring(bits from bit_pos for 56)::bigint::numeric * pow(2::numeric, exponent);
    exponent := exponent + 56;
  end loop;
  while bit_pos >= 8 loop
    bit_pos := bit_pos - 8;
    result := result + substring(bits from bit_pos for 8)::bigint::numeric * pow(2::numeric, exponent);
    exponent := exponent + 8;
  end loop;
  return trunc(result);
end;
$$;

In a future PostgreSQL version, when/if Dean Rasheed's patch 0001-Add-non-decimal-integer-support-to-type-numeric.patch gets committed, this can be simplified:

CREATE OR REPLACE FUNCTION numeric_from_bytes(bytea)
RETURNS numeric
LANGUAGE sql
AS $$
SELECT ('0'||right($1::text,-1))::numeric
$$;
Lambeth answered 23/1, 2023 at 19:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.