【问题标题】:Minimum bytes required to represent a signed hexadecimal value表示带符号的十六进制值所需的最小字节数
【发布时间】:2018-04-17 00:51:12
【问题描述】:

谁能告诉我 Perl 中有什么函数可以用最少的字节数将带符号的十进制转换为十六进制。

例如:-555(dec) => FFFFFDD5(hex) [使用 $Hex = sprintf("%X", -555)]

我希望结果是 FDD5 而不是 FFFFFDD5。

2 个字节足以表示 -555。但我得到 4 个字节的转换。

请帮忙!

【问题讨论】:

  • 我确定是XY-problem
  • 如果转换回来,你怎么知道FDD5-555而不是64981
  • 我完全同意前面的评论。 Just enough rope: perl -le 'print unpack("H*",pack("s>",-555))' 打印 fdd5 - 也可以尝试使用 pack 模式 "c""l>""q>"

标签: perl hex pack


【解决方案1】:

你的方法有两个问题。

  1. 无法解析。

    例如,即使您只知道自己有两个数字,
    字节 12 34 56 7816 可以指 1216 和 34567816
    字节 12 34 56 7816 可以指 123416 和 567816
    字节 12 34 56 7816 可以指 12345616 和 7816

    您可以使用一些外部方法来识别编码数字的长度,但这会使部分或全部节省无效。

  2. 这是模棱两可的。

    例如,
    Bytes FD D516 可以引用 6498110(Bytes 00 00 FD D516 作为 int32)。
    Bytes FD D516 可以引用 -55510(Bytes FF FF FD D516 作为 int32)。

一种解决方案是使用长度前缀(如 UTF-8)。

-2^13..2^13-1  2 bytes  00xx xxxx  xxxx xxxx
-2^21..2^21-1  3 bytes  01xx xxxx  xxxx xxxx  xxxx xxxx
-2^29..2^29-1  4 bytes  10xx xxxx  xxxx xxxx  xxxx xxxx  xxxx xxxx
-2^31..2^31-1  5 bytes  1100 0000  xxxx xxxx  xxxx xxxx  xxxx xxxx  xxxx xxxx

最佳方案将取决于您的数字分布。


上述方案的打包/编码函数可以写成如下:

sub pack_vint32 {
   my $n = shift;
   my $nn = $n >= 0 ? $n : ~$n;
   return substr(pack('L>', ($n & 0x3FFF    ) | 0x0000    ), -2) if !($nn & ~0x1FFF);
   return substr(pack('L>', ($n & 0x3FFFFF  ) | 0x400000  ), -3) if !($nn & ~0x1FFFFF);
   return substr(pack('L>', ($n & 0x3FFFFFFF) | 0x80000000), -4) if !($nn & ~0x1FFFFFFF);
   return "\xC0".pack('L>', $n);
}

上述方案的解包/解码函数可以写成如下:

sub unpack_vint32 {
   for (shift) {
      if (/^[\x00-\x3F]/) {
         return if length() < 2;
         my $n = unpack('L>', "\x00\x00".substr($_, 0, 2, '')) & 0x3FFF;
         $n -= 0x4000 if $n & 0x2000;
         return $n;
      }
      elsif (/^[\x40-\x7F]/) {
         return if length() < 3;
         my $n = unpack('L>', "\x00".substr($_, 0, 3, '')) & 0x3FFFFF;
         $n -= 0x400000 if $n & 0x200000;
         return $n;
      }
      elsif (/^[\x80-\xBF]/) {
         return if length() < 4;
         my $n = unpack('L>', substr($_, 0, 4, '')) & 0x3FFFFFFF;
         $n -= 0x40000000 if $n & 0x20000000;
         return $n;
      }
      elsif (/^\xC0/) {
         return if length() < 5;
         return unpack('xl>', substr($_, 0, 5, ''));
      }
      elsif (length() == 0) {
         return;
      }
   }

   croak("Bad data");
}

测试:

my $s =
   join '',
      map { pack_vint32($_) }
         map { $_, -$_ }
            130, 555, 0x12, 0x345678, 0x12345678;

say length($s);
say sprintf("%v02X", $s);

while ( my ($n) = unpack_vint32($s) ) {
   say $n;
}

croak("Bad data") if length($s);

输出:

28
00.82.3F.7E.02.2B.3D.D5.00.12.3F.EE.80.34.56.78.BF.CB.A9.88.92.34.56.78.AD.CB.A9.88
----- ----- ----- ----- ----- ----- ----------- ----------- ----------- -----------
130     |     |     |     |     |        |           |           |           |
-130 ---+     |     |     |     |        |           |           |           |
555 ----------+     |     |     |        |           |           |           |
-555 ---------------+     |     |        |           |           |           |
18 -----------------------+     |        |           |           |           |
-18 ----------------------------+        |           |           |           |
3430008 ---------------------------------+           |           |           |
-3430008 --------------------------------------------+           |           |
305419896 -------------------------------------------------------+           |
-305419896 ------------------------------------------------------------------+

【讨论】:

    【解决方案2】:

    我认为问题需要十六进制的字符串 (ASCII) 表示形式,它采用二进制补码中的最小字节数。我同意 cmets 和另一个关于这个模棱两可的答案,但对于“人类”消费来说可能没问题。在这种情况下:

    sub min_bytes {
      my $n = shift;
    
      my $s = 256; # Fits in one byte
      my $i = 1;   # Bytes counter
      while( 1 ) {
        if( $n < 0 && -$n <= $s / 2 ) {
          return $i;
        }
        elsif( $n >= 0 && $n < ($s / 2) - 1 ) {
          return $i;
        }
        $s *= 256;
        $i++;
      }
    }
    
    sub to_hex {
      my $n = shift;
    
      my $l = min_bytes($n);
      my $h = $n > 0 ? $n : (256 ** $l) + $n;
    
      my $s = '';
      for( my $i = 0; $i < $l; $i++ ) {
        $s = unpack('H2', pack('C', $n % 256)) . $s;
        $n = $n >> 8;
      }
    
      return $s;
    }
    
    # Let's try a few numbers
    my @numbers = (-10, -555, -100000, -100000000, -10000000000, -10000000000000);
    
    for my $n (@numbers) {
      my $l = min_bytes($n);
      my $h = to_hex($n);
      print $n, " takes ", $l, " byte(s) and looks like ", uc $h, "\n";
    }
    

    我尽量不使用任何复杂的东西。 unpack('H2', pack('C', $n % 256)) 位将单字节十进制数转换为十六进制数。

    打印出来:

    -10 takes 1 byte(s) and looks like F6
    -555 takes 2 byte(s) and looks like FDD5
    -100000 takes 3 byte(s) and looks like FE7960
    -100000000 takes 4 byte(s) and looks like FA0A1F00
    -10000000000 takes 5 byte(s) and looks like FDABF41C00
    -10000000000000 takes 6 byte(s) and looks like F6E7B18D6000
    

    代码使用 Perl 算法,如果数字大于内部表示的精度,可能会给出错误的结果。

    【讨论】:

    • @ikegami,我喜欢你的回答,但我认为我们阅读问题的方式不同。我理解它的方式,OP希望使用最小字节数的数字的十六进制表示。对于-555 想要FDD5 而不是FFFFFDD5。这用两个字节表示,正如您所指出的,十六进制是四个字节。当然,我可能错了……
    • 您的解决方案将 -555 转换为 4 个字节,但我相信希望将其转换为 2 个字节。
    • 您的程序与尺寸有关。例如,-555 在您的程序中占用四个字节,但程序声称它只占用两个字节。要获得正确的大小,请使用length($s)。 (这已修复,但修复已恢复???)
    • 返回的大小是使用二进制补码存储数字所需的最小字节数。这用于生成与十六进制表示的相同数字的字符串。
    • Re "返回的大小是使用二进制补码将数字存储为二进制所需的最小字节数。",我知道,这与我的任何事情都不矛盾说。也许您应该将输出更改为“-10 将占用 1 个字节作为二进制补全数”?但最好报告程序实际做了什么,而不是报告它可以做什么。
    猜你喜欢
    • 2012-04-16
    • 1970-01-01
    • 1970-01-01
    • 2018-08-04
    • 2014-12-20
    • 1970-01-01
    • 2018-08-07
    • 2011-12-06
    • 2020-06-03
    相关资源
    最近更新 更多