Author: Jonas Bilinkevicius
I have a 20 digit string, all numbers, and I would like to convert this to a Base36
to take up less space. I have tried the Borland Radix() routine but this would not
work on such a large number. Does anyone have an idea on how to convert the decimal
number string to aBase36 number string?
Answer:
Solve 1:
Does the encoding have to result in a string having only "printable" characters
(#32..#126) or is any byte value allowed? If so an easy packing method not
requiring any complex calculation would be BCD: pack two digits into a byte, giving
a 50% size reduction:
1 function NumStringToBCD(const inStr: string): string;
2
3 function Pack(ch1, ch2: Char): Char;
4 begin
5 Assert((ch1 >= '0') and (ch1 <= '9'));
6 Assert((ch2 >= '0') and (ch2 <= '9'));
7 {Ord('0') is $30, so we can just use the low nybble of the character as value.}
8 Result := Chr((Ord(ch1) and $F) or ((Ord(ch2) and $F) shl 4))
9 end;
10
11 var
12 i: Integer;
13 begin
14 if Odd(Length(inStr)) then
15 Result := NumStringToBCD('0' + inStr)
16 else
17 begin
18 SetLength(Result, Length(inStr) div 2);
19 for i := 1 to Length(Result) do
20 Result[i] := Pack(inStr[2 * i - 1], inStr[2 * i]);
21 end;
22 end;
23
24 function BCDToNumString(const inStr: string): string;
25
26 procedure UnPack(ch: Char; var ch1, ch2: Char);
27 begin
28 ch1 := Chr((Ord(ch) and $F) + $30);
29 ch2 := Chr(((Ord(ch) shr 4) and $F) + $30);
30 Assert((ch1 >= '0') and (ch1 <= '9'));
31 Assert((ch2 >= '0') and (ch2 <= '9'));
32 end;
33
34 var
35 i: Integer;
36 begin
37 SetLength(Result, Length(inStr) * 2);
38 for i := 1 to Length(inStr) do
39 UnPack(inStr[i], Result[2 * i - 1], Result[2 * i]);
40 end;
41
42 procedure TForm1.Button1Click(Sender: TObject);
43 var
44 S1, S2: string;
45 begin
46 S1 := '15151515151515151515';
47 S2 := NumStringToBCD(S1);
48 memo1.lines.add('S1: ' + S1);
49 memo1.lines.add('Length(S2): ' + IntToStr(Length(S2)));
50 memo1.lines.add('S2 unpacked again: ' + BCDToNumString(S2));
51 end;
Solve 2:
This DecimalStrToBase36Str seems to work on smaller inputs, but I suggest that you
check output on the larger inputs.
52 { ... }
53 const
54 Base36Digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
55
56 type
57 tArrayElement = Byte;
58 tDoubleElement = Word;
59 {tArrayElement = Word;}
60 {tDoubleElement = LongWord;}
61
62 const
63 SizeOfAryElem = SizeOf(tArrayElement);
64 BitsInBufElem = SizeOfAryElem * 8;
65
66 function DecimalStrToBase36Str(const Value: string): string;
67 var
68 Man: array[0..19] of tArrayElement;
69 NbrManElem, Cry, i, j, n, Tmp: integer;
70 Tmp1, Tmp2: packed record
71 case byte of
72 0: (Wd: tDoubleElement);
73 1: (Lo, Hi: tArrayElement);
74 end;
75 begin
76 n := length(Value);
77 if n <> 20 then
78 raise Exception.CreateFmt('Input string must be 20 decimal digits, not %d
79 digits'
80 [n]);
81 NbrManElem := 0;
82 for i := 1 to n do
83 begin
84 Cry := ord(Value[i]) - ord('0');
85 if (Cry < 0) or (Cry > 9) then
86 raise Exception.CreateFmt('Input string contains non-decimal digit (%s)',
87 [Value[i]]);
88 {Multiply accumulation by 10 and add k:}
89 for j := 0 to NbrManElem - 1 do
90 begin
91 Tmp := Man[j] * 10 + Cry;
92 Man[j] := Tmp and $FF;
93 Cry := Tmp shr 8;
94 end;
95 if Cry <> 0 then
96 begin
97 Inc(NbrManElem);
98 Man[NbrManElem - 1] := Cry;
99 end;
100 end;
101 SetLength(Result, 14);
102 for i := 14 downto 1 do
103 begin
104 {Divide by 36 and save the remainder:}
105 Tmp1.Hi := 0;
106 for j := NbrManElem - 1 downto 0 do
107 begin
108 Tmp1.Lo := Man[j];
109 Tmp2.Wd := Tmp1.Wd div 36;
110 Assert(Tmp2.Hi = 0);
111 Man[j] := Tmp2.Lo;
112 Tmp1.Hi := Tmp1.Wd mod 36;
113 end;
114 Result[i] := Base36Digits[Tmp1.Hi + 1];
115 if (NbrManElem > 0) and (Man[NbrManElem - 1] = 0) then
116 begin
117 dec(NbrManElem);
118 end;
119 end;
120 end;
|