Defining FixedPoint binary arithmetic operators

The sole reason for defining a new class of numbers is to overload the arithmetic operators. Each FixedPoint object has two parts: value and scale. We can say some value, , is a fraction of the value  divided by the scaling factor : .

Note that we've worked out the algebra in the following example using correct, but inefficient, floating point expressions, we'll discuss the slightly more efficient, pure integer operations.

The general form for addition (and subtraction) is this: , which can create a result with a lot of useless precision.

Imagine adding 9.95 and 12.95. We'd have (in principle) 229000/10000. This can be properly reduced to 2290/100. The problem is that it also reduces to 229/10, which is no longer in cents. We'd like to avoid reducing fractions in a general way and instead stick with cents or mills to the extent possible.

We can identify two cases for :

  • The scale factors match: In this case,  and the sum is . When adding a FixedPoint value and a plain old integer value, this will also work. We can force the integer to have the required scale factor.

  • The scale factors don't match: When , it can help to produce a result scale  with the maximum scale factor of the two input values: . From this, we can compute two intermediate scale factors: and . One of those scale factors will be 1, and the other will be less than 1. We can now add with a common value in the denominator. Algebraically, it's . This can be further optimized into two cases, since one of the factors is 1 and the other is a power of 10.

We can't really optimize multiplication. It's . The precision really must increase when we multiply the FixedPoint values.

Division is multiplication by an inverse: . If A and B have the same scale, these values will cancel so that we do have a handy optimization available. However, this changes the scale from cents to wholes, which might not be appropriate.

The following is what the forward operators, built around a similar boilerplate, look like:

def __add__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':
if not isinstance(other, FixedPoint):
new_scale = self.scale
new_value = self.value + other * self.scale
else:
new_scale = max(self.scale, other.scale)
new_value = self.value * (new_scale // self.scale) + other.value * (
new_scale // other.scale
)
return FixedPoint(int(new_value), scale=new_scale)

def __sub__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':
if not isinstance(other, FixedPoint):
new_scale = self.scale
new_value = self.value - other * self.scale
else:
new_scale = max(self.scale, other.scale)
new_value = self.value * (new_scale // self.scale) - other.value * (
new_scale // other.scale
)
return FixedPoint(int(new_value), scale=new_scale)

def __mul__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':
if not isinstance(other, FixedPoint):
new_scale = self.scale
new_value = self.value * other
else:
new_scale = self.scale * other.scale
new_value = self.value * other.value
return FixedPoint(int(new_value), scale=new_scale)

def __truediv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':
if not isinstance(other, FixedPoint):
new_value = int(self.value / other)
else:
new_value = int(self.value / (other.value / other.scale))
return FixedPoint(new_value, scale=self.scale)

def __floordiv__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':
if not isinstance(other, FixedPoint):
new_value = int(self.value // other)
else:
new_value = int(self.value // (other.value / other.scale))
return FixedPoint(new_value, scale=self.scale)

def __mod__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':
if not isinstance(other, FixedPoint):
new_value = (self.value / self.scale) % other
else:
new_value = self.value % (other.value / other.scale)
return FixedPoint(new_value, scale=self.scale)

def __pow__(self, other: Union['FixedPoint', int]) -> 'FixedPoint':
if not isinstance(other, FixedPoint):
new_value = (self.value / self.scale) ** other
else:
new_value = (self.value / self.scale) ** (other.value / other.scale)
return FixedPoint(int(new_value) * self.scale, scale=self.scale)

For the simple addition, subtraction, and multiplication cases, we've provided versions that can be optimized to eliminate some of the relatively slow floating point intermediate results.

Each of these operators returns an instance of the FixedPoint class. We can't use the name inside the class definition itself. We've provided a string version of the name. The mypy utility will resolve this string to the proper type name when it is used to check the type hints.

In some cases, we've used Union['FixedPoint', int] to explicitly support integer coercion.  This type hint tells that mypy the method will accept either an instance of the FixedPoint class or a simple int object. 

For the two divisions, the __mod__(), and __pow__() methods, we haven't done any optimization to try and eliminate noise being introduced via floating-point division. Instead, we've provided a working Python implementation that can be used with a suite of unit tests as a basis for optimization and refactoring.

It's important to note that the division operations can properly reduce the scale factors. However, changing the scale may be undesirable. When doing currency work, we might divide the currency rate (dollars) by a non-currency value (hours) to get the dollars-per-hour result. The proper answer might have zero relevant decimal places.  This would be a scale of 1, but we might want to force the value to have a cents-oriented scale of 100. This implementation ensures that the left-hand side operand dictates the desired number of decimal places.

Now, let's see how to define FixedPoint unary arithmetic operators.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset