In a continuing effort to move towards standards compliance, the Smalltalk team at GemStone has recently been looking at ScaledDecimal.

Backwards Compatibility

GemStone/S has a class named ScaledDecimal and the current implementation, while not ANSI-compliant, is useful and will likely remain but with a new name (say, ScaledFraction). Instances of the legacy class could be created using Number>>#'asScaledFraction:' or by sending #'fromString:' to the legacy class.

The remaining discussion concerns the new implementation of an ANSI-compliant scaledDecimal (or a mostly-compliant implementation; see the questions at the end).

Defined Protocols

According to ANSI Smalltalk, the scaledDecimal protocol provides “a numeric representation of fixed point decimal numbers.” The protocol defines only one message, #'scale', that answers an ‘integer which represents the total number of digits used to represent the fraction part of the receiver, including trailing zeros.” This implies that the value of any fractional digits beyond the scale is zero (since there is no representation for those digits). That is, multiplying any scaledDecimal by its scale would produce a number in which the fractional part is always exactly zero.

A scaledDecimal can be created by explicitly defining a numeric literal using the format scaledMantissa 's' [fractionalDigits] (e.g., 123.45s4) where the resulting scaledDecimal has a scale of fractionalDigits (here 4 even though only two digits were provided to the right of the decimal). ANSI specifies that it is an error for the scaledMantissa to provide more digits to the right of the decimal than fractionalDigits. If the optional fractionalDigits are not present then the scale is the number of digits to the right of the decimal in scaledMantissa.

A scaledDecimal can also be created by sending the message #'asScaledDecimal:' to a number and providing ‘a fractional precision’ as an argument. The fractional precision specified might be less than the count of digits to the right of the decimal point in a decimal representation of the number (which could be infinite in the case of a fraction such as one-third), in which case the result will be rounded. ANSI does not specify the rounding rule here but does provide one in the definition for number>>#'rounded'.

ANSI specifies a refinement of #'asScaledDecimal:' when the receiver is an integer. No mater what value is provided as the argument, ANSI states that the resulting scaledDecimal has a scale of zero. The usefulness of this is questionable and it would result in inconsistent results if one were, say, printing an expression (e.g., (x integerPart / 2) asScaledDecimal: 2). Even VA Smalltalk (which is the most conforming I’ve found) does not follow this.

Scale for Result

Many messages defined in the number protocol can return a scaledDecimal, including *, +, -, /, \\, integerPart, quo:, reciprocal, rem:, and roundTo:. In most of these cases, ANSI specifies that the scale of the result is at least the scale of the receiver. Thus, the actual scale is largely implementation defined. There are a few options:

  • Select the receiver’s scale. This allows the receiver to control the outcome, which is at least an easy rule and follows the OO paradigm of messages rather than operators but is inconsistent with much of the numeric protocol where the returned value is based on the receiver and argument, not just the receiver (an integer plus a fraction returns a fraction).
  • Select the smaller scale (if allowed). On something like addition this would be a way to avoid implying that the result is more accurate than it actually is. For example, 0.33s2 + 0.0001s4 might be better represented as 0.33s2 since the receiver, by definition, is ignoring anything more than two digits to the right of the decimal. Note that ANSI specifies that the result will have a scale of at least that of the receiver, so switching the order of the addition would change the result, leading to potential surprises, particularly when dealing with variables with passed-in or computed values.
  • Select the larger scale.
  • Vary the selection based on the operator. For example, 0.1s * 0.2s could be 0.0s1 or 0.02s2.

Whatever rule was provided by default, the programmer could work around it by changing the scale of the receiver, the argument, or the result. Thus, the goal should be to identify the typical use or expected behavior to reduce the typing required and the surprise.

Some Other Dialects

Since GemStone has libraries for a couple other Smalltalk dialects, it seems valuable to compare them.

VisualWorks 7.6 (from Cincom Smalltalk) seems to be the most distant from ANSI. The numeric literal with ‘s’ results in an instance of FixedPoint that is implemented much like GemStone’s legacy ScaledDecimal. That is, the internal representation is a Fraction and the scale is available to provide support for printing and explicit rounding. VW 7.6 does not support #'asScaledDecimal:' but does have #'asFixedPoint:'. Because scale does not actually mean the count of digits used to represent the fractional precision, the scale can be changed at any time without any impact on the precision. When a scaledDecimal is the result of a message to a number, the resulting scale is typically the maximum of the receiver’s and argument’s scale.

VA Smalltalk 7.5 (from Instantiations) seems to have the most compliant implementation. All scaledDecimals are represented internally as binary-coded-decimal in 18-bytes with up to 31 digits of precision and a maximum scale of 30. This seems to be quite close to the typical Cobol implementation, right down to the sign byte of C or D (credit/debit!). The scale actually identifies the count of digits for the internal representation. When a scaledDecimal is the result of a message to a number, the resulting scale is adjusted based on the message. For example, 0.1s1 * 0.2s1 = 0.02s2. That is, truncation/rounding is avoided whenever possible (up to the maximum precision/scale).

Notes

An implementation based on an integer with a decimal power of ten scale could be faster than the current fraction-based approach since some normalization could be avoided.

With ScaledFraction intermediate values are infinite precision and rounding/truncating is done when explicitly requested. One also could have a conforming ScaledDecimal implementation with minimal intermediate rounding/truncating (see, e.g., VA Smalltalk), though it seems that some limit (other than positive infinity) needs to be placed on the scale.

The fact that the fraction-based approach is useful does not mean that an implementation following ANSI would be of no value. The fact that no one is using it might be due to the fact that it doesn’t exist.

Questions:

  1. How much disruption would be caused by moving pre-existing objects and behavior to a differently-named class? The impact would be on code with ‘s’ numeric literals, senders of #'asScaledDecimal:', and explicit class references.
  2. Should specifying more than fractionalDigits digits to the right of the scaledMantissa’s decimal (e.g., 123.456s2) result in an error (as required by ANSI and implemented by VW)? If not, how should the scale be determined?
  3. When creating a scaledDecimal from a number using #'asScaledDecimal:' what rounding rule should be used when the value is exactly half way between two allowed results? Round away from zero? Round to an even least significant digit?
  4. When #'asScaledDecimal:' is sent to an integer, should the result have a scale of zero (per ANSI) or a scale of the provided argument (avoiding surprises)?
  5. What rounding, if any, should take place on intermediate values? More specifically, when a scaledDecimal is returned from messages like multiplication in the number protocol, what should be the scale of the result?
  6. What maximum limit, if any, should be placed on scale? If one tries to avoid rounding intermediate results, then some limit is probably necessary. Would it be important to try to keep things in the SmallInteger range? GemStone has a limit on LargeIntegers of several thousand digits. It would seem reasonable to avoid spilling over to that accidentally.
  7. What name would be appropriate for the legacy implementation? Would following VW’s (mis-named) FixedPoint increase or reduce confusion? Would FractionWithScale be more descriptive?

Comments may be added here or sent to the GemStone user’s mailing list.

Advertisements