For this to solve we need to cut it down into three steps:
- Converting the hexadecimal number digit by digit. This leads into solving how to
- multiply decimal numbers, which in turn needs us being able to
- add decimal numbers digit by digit.
As tiny helpers which are needed in the other functions let me have these:
type TArrayString= Array of String; // In case it doesn't exist already // Fill a text with leading zeroes up to the desired length procedure MinLen( var s: String; iLen: Integer ); begin while Length( s )< iLen do s:= '0'+ s; end;
Addition
We all learnt written addition in school: write all numbers in one row, then add each row's digits and carry that sum's additional digits over to the next row of digits of the summands. This can also be done easily:
// Addition of multiple long numbers function Summe( aSummand: TArrayString ): String; var iLenMax, iA, iPos, iSum, iAdvance: Integer; c: Char; begin result:= '0'; case Length( aSummand ) of 0: exit; // Nothing to add at all 1: begin result:= aSummand[Low( aSummand )]; // Sum equals the only summand exit; end; end; // Find the longest text, then make all texts as long as the longest, // so we can simply access an existing character at that position. iLenMax:= 0; for iA:= Low( aSummand ) to High( aSummand ) do begin if Length( aSummand[iA] )> iLenMax then iLenMax:= Length( aSummand[iA] ); end; for iA:= Low( aSummand ) to High( aSummand ) do MinLen( aSummand[iA], iLenMax ); MinLen( result, iLenMax ); // All have the same length: process from the least significant digit // (right) to the most significant digit (left). for iPos:= iLenMax downto 1 do begin // Manual addition: write all numbers in one row, then add single // digits per row. Nobody will ever give this function so many // summands that the sum of single digits will come near the Integer // capacity. iSum:= 0; for iA:= Low( aSummand ) to High( aSummand ) do begin Inc( iSum, Ord( aSummand[iA][iPos] )- $30 ); // Add digit from each number end; Inc( iSum, Ord( result[iPos] )- $30 ); // Also add result's digit from potential previous carry // Turn that Integer sum into text again, digit by digit iAdvance:= 0; // Exceeding the current position if we need to carry while iSum> 0 do begin c:= Chr( (iSum mod 10)+ $30 ); // Only the rightmost digit if iPos- iAdvance< 1 then begin // Outside the String? result:= c+ result; // Prepend end else begin result[iPos- iAdvance]:= c; // Set new digit in overall sum end; iSum:= iSum div 10; // This digit has been process, go to next one Inc( iAdvance ); // Not in the current position anymore, but processing potential carries end; end; end;
I did not restrict it to always 2 summands for the following reasons:
- Dealing with an unknown amount of summands needs virtually no extra work.
- Adding multiple summands (instead of always 2) in one go is more efficient than calling this function again and again.
- Later with multiplication we can lazily call this function once with f.e. 6 times the same summand to simulate multiplication by 6.
$30 is the ASCII code for the character '0' - subtracting the potential character '0' to '9' by that of '0' gives us the value 0 to 9.
Multiplication
We all learnt written multiplication in school, too: for each digit of one factor calculate that product (which can only be an "easy" multiplication by 0 to 9), write down all those products in a row as per digit position (optionally with trailing zeroes), then add all those products into a sum (referencing written addition). This can also be done easily, since we now have solved addition already:
// Multiplication of two long numbers function Produkt( s1, s2: String ): String; var iPos, iStep, iA, iNine: Integer; aSummand, aStep: TArrayString; begin // For each digit of one factor we will make a separate multiplication SetLength( aSummand, Length( s1 ) ); // This time it doesn't matter how long both numbers are: just again go // from least significant digit (right) to most significant digit (left). for iPos:= Length( s1 ) downto 1 do begin iA:= Length( s1 )- iPos; // Array index per digit // As per our position the sum must be shifted by 10: first (rightmost) // digit needs no shift (0), second needs one (10), third needs two (100)... MinLen( aSummand[iA], iA ); // Current digit iStep:= Ord( s1[iPos] )- $30; case iStep of 0: ; // Multiplication by 0 always results in 0, an empty summand equals one of "0" 1: aSummand[iA]:= s2+ aSummand[iA]; // No multiplication needed, just prepend with factor else // Cheap multiplication: use addition with 2 to 9 times the same summand SetLength( aStep, iStep ); for iNine:= 0 to iStep- 1 do aStep[iNine]:= s2; aSummand[iA]:= Summe( aStep )+ aSummand[iA]; // Prepend sum, zeroes must be trailing end; end; // Now just add all sums that we got per digit result:= Summe( aSummand ); end;
It could have been even shorter, since Summe() can already deal with 0 and 1 summands - I don't really need to treat that differently. As previously told: the easy multiplication is done by simple addition - not very performance efficient, but easy to comprehend.
Hexadecimal to decimal conversion
Since we can now add and multiply, we're also able to convert each digit of a non-decimal-base number and increase the outcome to the overall result:
// Convert base 2..36 long number into base 10 long number function ConvertToDecimal( sFrom: String; sBase: String= '16' ): String; var sStep, sPos: String; cFrom: Char; a: TArrayString; begin SetLength( a, 2 ); a[0]:= '0'; // Overall sum = result sPos:= '1'; // Current digit's power while Length( sFrom )> 0 do begin // Process least significant digit (right) cFrom:= sFrom[Length( sFrom )]; case cFrom of // For now at max base 36 is supported, which includes hexadecimal '0'.. '9': sStep:= cFrom; // Same as text 'A'.. 'Z': sStep:= IntToStr( Ord( cFrom )- $41+ 10 ); // Turn "B" into "11" end; a[1]:= Produkt( sPos, sStep ); // Multiply current digit's value by current position's power a[0]:= Summe( a ); // Add both product and current overall result sPos:= Produkt( sPos, sBase ); // Increase power to next digit position Delete( sFrom, Length( sFrom ), 1 ); // Remove rightmost digit end; result:= a[0]; end;
It even works not only for hexadecimal (base 16) input, but also for others. $41 is the ASCII value for 'A' - subtracting the potential characters 'A' to 'Z' by that of 'A' gives us the value 0 to 25, to which we then just add 10.
Tests
Strings are implied. Spaces are just for optical reasons.
| function | parameters | result | proof |
Summe() | 123 + 456 | 579 | brain |
Summe() | 123 + 456 + 9999 | 10578 | MS calc, brain |
Produkt() | 36 * 12 | 504 | MS calc, brain |
Produkt() | 8 6426578999 * 9731421999 | 8 4105351216 9179999001 | rapidtables.com |
ConvertToDecimal() | 6F0000 80B4D4B3 426C66A6 55010001 000080B4 | 247538 2888117010 1369500890 2692616764 2744062132 | rapidtables.com |
Summary
- This is a good exercise on turning basic/existing skills (arithmetics) into program code in the sense of needing more understanding in logics than in programming/the language, making it appealing to both beginners (still struggling with programming) and experts (mostly focusing on task automation).
- Performance wise this is surely far from optimal, but should be rather easy to understand/comprehend/follow instead of being difficult to read. Likewise it should be easy to translate into other languages.
- A bonus exercise would be to also add subtraction and division.
- A few tiny details are omitted, f.e. numbers are always expected to not have leading zeroes - the results may then also have unneeded leading zeroes. Empty Strings are interpreted as
0, but a result of 0 is not reduced to an empty String either. - It works with any String type, because we only operate on ASCII anyway - there's no need to distinguish between
AnsiString, UnicodeString, WideString and so on. However, Char is used as type, which is bound to that choice. But the functions Chr() and Ord() should support everything again.
UInt64.UInt64in Delphi 7 but it not working as expected for example ifuisUInt64and triedu := $FFFFFFFFFFFFFFFF;you will get error ": Constant expression violates subrange bounds". Even if UInt64 was working it will not solve my problem.