{fixed,round}Tools: factor out nearestMultipleShortestRepr

extract the code from fixedToStr into its own function (defined in roundTools), to be used for serializing fractional F2Dot14 angles for COLRv1 PaintRotate etc.
This commit is contained in:
Cosimo Lupo 2021-07-02 18:15:32 +01:00
parent aae11e45c2
commit f05691246a
2 changed files with 53 additions and 24 deletions

View File

@ -17,7 +17,7 @@ functions for converting between fixed-point, float and string representations.
The maximum value that can still fit in an F2Dot14. (1.99993896484375)
"""
from .roundTools import otRound
from .roundTools import otRound, nearestMultipleShortestRepr
import logging
log = logging.getLogger(__name__)
@ -125,6 +125,7 @@ def fixedToStr(value, precisionBits):
This is pretty slow compared to the simple division used in ``fixedToFloat``.
Use sporadically when you need to serialize or print the fixed-point number in
a human-readable form.
It uses nearestMultipleShortestRepr under the hood.
Args:
value (int): The fixed-point value to convert.
@ -133,27 +134,8 @@ def fixedToStr(value, precisionBits):
Returns:
str: A string representation of the value.
"""
if not value: return "0.0"
scale = 1 << precisionBits
value /= scale
eps = .5 / scale
lo = value - eps
hi = value + eps
# If the range of valid choices spans an integer, return the integer.
if int(lo) != int(hi):
return str(float(round(value)))
fmt = "%.8f"
lo = fmt % lo
hi = fmt % hi
assert len(lo) == len(hi) and lo != hi
for i in range(len(lo)):
if lo[i] != hi[i]:
break
period = lo.find('.')
assert period < i
fmt = "%%.%df" % (i - period)
return fmt % value
return nearestMultipleShortestRepr(value/scale, factor=1.0/scale)
def strToFixed(string, precisionBits):
@ -214,7 +196,7 @@ def floatToFixedToStr(value, precisionBits):
This uses the shortest decimal representation (ie. the least
number of fractional decimal digits) to represent the equivalent
fixed-point number with ``precisionBits`` fractional binary digits.
It uses fixedToStr under the hood.
It uses nearestMultipleShortestRepr under the hood.
>>> floatToFixedToStr(-0.61883544921875, precisionBits=14)
'-0.61884'
@ -227,8 +209,8 @@ def floatToFixedToStr(value, precisionBits):
str: A string representation of the value.
"""
fixed = otRound(value * (1 << precisionBits))
return fixedToStr(fixed, precisionBits)
scale = 1 << precisionBits
return nearestMultipleShortestRepr(value, factor=1.0/scale)
def ensureVersionIsLong(value):

View File

@ -56,3 +56,50 @@ def roundFunc(tolerance, round=otRound):
return round
return functools.partial(maybeRound, tolerance=tolerance, round=round)
def nearestMultipleShortestRepr(value: float, factor: float) -> str:
"""Round to nearest multiple of factor and return shortest decimal representation.
This chooses the float that is closer to a multiple of the given factor while
having the shortest decimal representation (the least number of fractional decimal
digits).
For example, given the following:
>>> nearestMultipleShortestRepr(-0.61883544921875, 1.0/(1<<14))
'-0.61884'
Useful when you need to serialize or print a fixed-point number (or multiples
thereof, such as F2Dot14 fractions of 180 degrees in COLRv1 PaintRotate) in
a human-readable form.
Args:
value (value): The value to be rounded and serialized.
factor (float): The value which the result is a close multiple of.
Returns:
str: A compact string representation of the value.
"""
if not value:
return "0.0"
value = otRound(value / factor) * factor
eps = .5 * factor
lo = value - eps
hi = value + eps
# If the range of valid choices spans an integer, return the integer.
if int(lo) != int(hi):
return str(float(round(value)))
fmt = "%.8f"
lo = fmt % lo
hi = fmt % hi
assert len(lo) == len(hi) and lo != hi
for i in range(len(lo)):
if lo[i] != hi[i]:
break
period = lo.find('.')
assert period < i
fmt = "%%.%df" % (i - period)
return fmt % value