Since you are dealing with floating point, be aware that the algorithm which will give you a correctly rounded result is not as trivial as one may think.
Also, depending on the Smalltalk dialect and its version, there is no guaranty that:
- conversion from ASCII decimal to binary Float is correctly rounded
- conversion from binary Float to ASCII decimal is correctly rounded
At least, in a recent Squeak or Pharo image the two operations above will be correctly rounded. All the explanation below is therefore built in Squeak 5.x or 6.x.
The first problem encountered is that the binary Float cannot represent exactly the two decimal quantities 0.014545 and 0.018045.
In Squeak, those two expressions are false:
(Fraction readFrom: '0.014545' readStream) isAnExactFloat.
(Fraction readFrom: '0.018045' readStream) isAnExactFloat.
In Squeak, those two inequalities are true, which may give a clue about the rounding direction used when rounding the decimal quantities to the nearest (representable) Float:
(Float readFrom: '0.014545' readStream) > (Fraction readFrom: '0.014545' readStream).
(Float readFrom: '0.018045' readStream) < (Fraction readFrom: '0.018045' readStream).
In Squeak, a Float print as the shortest decimal representation that would convert back to the same Float (round trip conversion), not the exact decimal value carried by the Float, so you cannot trust what you see.
The exact values are given by:
(0.014545 asTrueFraction printShowingMaxDecimalPlaces: Float precision - Float emin - 1)
-> '0.01454500000000000063671290462252727593295276165008544921875'
(0.018045 asTrueFraction printShowingMaxDecimalPlaces: Float precision - Float emin - 1)
-> '0.018044999999999998541166945642544305883347988128662109375'
Moreover, a multiplication by 100 may change the rounding... This is because the exact result may not necessarily be representable as a Float:
In Squeak, those two expressions evaluate to false:
((Float readFrom: '0.014545' readStream) asTrueFraction * 100) isAnExactFloat.
((Float readFrom: '0.018045' readStream) asTrueFraction * 100) isAnExactFloat.
This means that there will be another rounding to nearest representable Float. Luckily here, the multiplication did preserve the two inequalities that we introduced above:
(Float readFrom: '0.014545' readStream) * 100 > (Fraction readFrom: '1.4545' readStream).
(Float readFrom: '0.018045' readStream) * 100 < (Fraction readFrom: '1.8045' readStream).
But it's not necessarily the case:
((014545 to: 018045 by: 10) detect: [:x | (x/1000000) asFloat < (x/1000000) and: [(x/1000000) asFloat * 100 > (x/1000000 * 100)]])
-> 14575
You see that the multiplication by 100 changes the apparent rounding direction:
(Float readFrom: '0.014575') < (Fraction readFrom: '0.014575').
(Float readFrom: '0.014575') * 100 > (Fraction readFrom: '1.4575').
(0.014575 asTrueFraction printShowingMaxDecimalPlaces: Float precision - Float emin - 1)
-> '0.01457499999999999941435735451022992492653429508209228515625'
((0.014575*100) asTrueFraction printShowingMaxDecimalPlaces: Float precision - Float emin - 1)
-> '1.457500000000000017763568394002504646778106689453125'
So you cannot rely on a multiplication by 100 either...
Moreover, you did not tell how you rounded to 3 decimals...
The rounding operation shown in introduction is not exact either, there are tricky edge cases introduced by floating point.
For example we have:
0.5 predecessor < 0.5.
We thus expect 0.5 predecessor rounded = 0, which is fortunately true in Squeak.
But we have:
(0.5 predecessor + 0.5) truncated = 1.
Not (always) a correct implementation of rounded...
If your Smalltalk dialect provide some kind of decimal instead of Float (Squeak and Pharo have ScaledDecimal), I would advise to stick to those decimals.
For example you can write:
((0.014545s * 100) printShowingDecimalPlaces: 3) , '%'.
((0.018045s * 100) printShowingDecimalPlaces: 3) , '%'.
If you really want to implement the operation for Float, you can try to convert the Float to a Fraction as early as possible, so as to stick to exact arithmetic and not introduce additional rounding:
^(self asTrueFraction * 100 printShowingDecimalPlaces: 3) , '%'
In which case, you'll get '1.804%', which is fair...
Above implementation is not particularly fast, but it's simple enough.
And if you want something really tricky, you may try:
"re-interpret self printed representation as a Fraction.
This may alter the exact value and rounding"
^((Fraction readFromString: self printString) * 100 printShowingDecimalPlaces: 3) , '%'
In other dialects that may contain spurious inexactly rounded operations, implementing the algorithm correctly for Float might be hard!