Module refinery.lib.inno.delphi.numbers
Delphi's text of numbers and its parsing of numbers: Val, Str, the SysUtils functions that
convert integers, and FloatToText, which formats floating-point values with regional settings.
Expand source code Browse git
"""
Delphi's text of numbers and its parsing of numbers: `Val`, `Str`, the SysUtils functions that
convert integers, and `FloatToText`, which formats floating-point values with regional settings.
"""
from __future__ import annotations
import enum
import functools
import json
import math
import re
from dataclasses import dataclass
from fractions import Fraction
from typing import TYPE_CHECKING, NamedTuple
from refinery.lib.inno.delphi import x87
from refinery.lib.inno.delphi.errors import EConvertError, SysConst, Undefined
from refinery.lib.inno.delphi.x87 import FPU, Real
from refinery.lib.resources import datapath
if TYPE_CHECKING:
from refinery.lib.inno.delphi.runtime import Runtime
@dataclass(frozen=True)
class FormatSettings:
"""
The regional settings that Delphi formats numbers with, named as the fields of Delphi's record
`TFormatSettings`. The defaults are those of English (United States).
"""
decimal_separator: str = '.'
thousand_separator: str = ','
currency_string: str = '$'
currency_format: int = 0
negative_currency_format: int = 0
currency_decimals: int = 2
@classmethod
def of_locale(cls, lcid: int) -> FormatSettings:
"""
The regional settings that Windows 11 has for the locale with the given ID, as Delphi reads
them: the first character of each separator.
"""
try:
_, *fields = _regional_formats()[F'{lcid:04x}']
except KeyError:
raise ValueError(F'Windows has no regional format for the locale 0x{lcid:04X}.') from None
return cls(*fields)
@functools.cache
def _regional_formats() -> dict[str, list]:
return json.loads(datapath('windows-locales.json').read_text(encoding='utf8'))
def val_integer(text: str, bits: int) -> tuple[int, int]:
"""
Delphi's `Val` for an integer of the given width: the value and the error code, which is 0 on
success and otherwise the position of the first character that is not part of the number,
counting from 1. Leading spaces are skipped; a sign may follow; `$`, `x`, `X`, `0x` and `0X`
start a hexadecimal number. A hexadecimal number may fill all bits, a decimal number must fit
the signed range.
"""
limit = (1 << (bits - 1)) - 1
wrap = 1 << bits
def at(position: int) -> str:
if 1 <= position <= len(text):
return text[position - 1]
return '\0'
def signed(value: int) -> int:
value %= wrap
return value - wrap if value > limit else value
if not text:
return 0, 1
position = 1
while at(position) == ' ':
position += 1
negative = False
if at(position) == '-':
negative = True
position += 1
elif at(position) == '+':
position += 1
result = 0
empty = True
first = at(position)
if first == '$' or first in 'xX' or first == '0' and position < len(text) and at(position + 1) in 'xX':
if first == '0':
position += 1
position += 1
while (digit := _HEX_DIGITS.get(at(position))) is not None:
if result < 0 or result > limit >> 3:
break
result = signed((result << 4) + digit)
position += 1
empty = False
if negative:
result = signed(-result)
else:
while (digit := _DECIMAL_DIGITS.get(at(position))) is not None:
if result < 0 or result > limit // 10:
break
result = signed(result * 10 + digit)
position += 1
empty = False
if negative:
result = signed(-result)
if result != 0 and negative != (result < 0):
position -= 1
if at(position) != '\0' or empty:
return result, position
return result, 0
def val_unsigned(text: str) -> tuple[int, int]:
"""
Delphi's `Val` for a UInt64: as for an Int64, but a decimal number may fill all 64 bits and only
zero may have a minus sign.
"""
stripped = text.lstrip(' ')
negative = stripped.startswith('-')
if negative:
stripped = stripped[1:]
elif stripped.startswith('+'):
stripped = stripped[1:]
offset = len(text) - len(stripped)
if not stripped:
return 0, offset + 1 if text else 1
hexadecimal = stripped[0] in '$xX' or stripped[:2] in ('0x', '0X')
body = stripped[2:] if stripped[:2] in ('0x', '0X') else stripped[1:] if hexadecimal else stripped
head = len(stripped) - len(body)
digits = _HEX_DIGITS if hexadecimal else _DECIMAL_DIGITS
base = 16 if hexadecimal else 10
value = 0
for k, character in enumerate(body):
digit = digits.get(character)
if digit is None:
return value, offset + head + k + 1
value = value * base + digit
if value >> 64:
return 0, offset + head + k + 1
if not body:
return 0, offset + head + 1
if negative and value:
return 0, 1
return value, 0
_DECIMAL_DIGITS = {chr(48 + k): k for k in range(10)}
_HEX_DIGITS = {
**_DECIMAL_DIGITS,
**{chr(65 + k): 10 + k for k in range(6)},
**{chr(97 + k): 10 + k for k in range(6)},
}
def str_to_int64(text: str) -> int:
"""
SysUtils' `StrToInt64`: raises an `EConvertError` for a text that `Val` does not read as an
Int64.
"""
value, error = val_integer(text, 64)
if error:
raise EConvertError(SysConst.SInvalidInteger % text)
return value
def str_to_int64_default(text: str, default: int) -> int:
"""
SysUtils' `StrToInt64Def`: the default for a text that `Val` does not read as an Int64.
"""
value, error = val_integer(text, 64)
return default if error else value
def str_to_uint64(text: str) -> int:
"""
SysUtils' `StrToUInt64`: raises an `EConvertError` for a text that `Val` does not read as a
UInt64.
"""
value, error = val_unsigned(text)
if error:
raise EConvertError(SysConst.SInvalidInteger % text)
return value
def str_to_uint64_default(text: str, default: int) -> int:
"""
SysUtils' `StrToUInt64Def`: the default for a text that `Val` does not read as a UInt64.
"""
value, error = val_unsigned(text)
return default if error else value
class Special(enum.Enum):
"""
The values of a float that have no digits.
"""
Infinity = 'INF'
NegativeInfinity = '-INF'
NaN = 'NAN'
@dataclass(frozen=True)
class Library:
"""
The routines of Delphi's run-time library that convert between floats and text, as Delphi builds
them for a platform. They compute on `unit` and write at most `digits` significant digits. They
scale by powers of ten from a table whose powers `10 ** (32 * k)` end at `k = table_end`. `Val`
gives an infinity when the exponent and the number of decimals add up to more than
`exponent_limit`, if there is a limit. The power `-2**31`, which the library cannot negate, gives
an infinity when `minimum_power_is_infinite` holds; otherwise the library reads a power after
the end of its table.
"""
unit: FPU
digits: int
table_end: int
exponent_limit: int | None
minimum_power_is_infinite: bool
@dataclass(frozen=True)
class Decimal:
"""
A float as decimal digits: the value is `0.digits * 10 ** exponent`, negative or not. Delphi's
record `TFloatRec` holds a float in this way.
"""
negative: bool
digits: str
exponent: int
def decimal(
value: Real,
precision: int,
decimals: int | None = None,
*,
library: Library,
denormal_is_zero: bool = True,
) -> Decimal | Special:
"""
Delphi's `FloatToDecimal`: the digits of the value rounded to the given number of significant
digits, at most those of the library, and to the given number of digits after the decimal point.
The digits come from `_stored_digits` and are rounded half up. Trailing zeros are removed; zero
has no digits, and so has a denormal Extended unless the caller is `Str`, which writes its
digits.
"""
if isinstance(value, float):
if math.isnan(value):
return Special.NaN
if math.isinf(value):
return Special.NegativeInfinity if value < 0 else Special.Infinity
negative = x87.is_negative(value)
if value == 0:
return Decimal(negative, '', 0)
mantissa, power = x87.parts(value)
if denormal_is_zero and abs(mantissa).bit_length() + power <= _EXTENDED_DENORMAL:
return Decimal(negative, '', 0)
text, exponent = _stored_digits(value, library)
return _rounded(negative, text, exponent, min(precision, library.digits), decimals)
def _stored_digits(value: Real, library: Library) -> tuple[str, int]:
"""
The 18 decimal digits that Delphi's `FloatToDecimal` stores for a finite value other than zero,
and the exponent of the value `0.digits * 10 ** exponent`. The library estimates the decimal
exponent from the binary one, multiplies by a power of ten to leave 18 integer digits, rounds
to an integer, divides by ten when that has 19 digits, and stores the integer as 18 digits. The
estimate is one too high for some small values, whose digits then start with a zero.
"""
unit = library.unit
mantissa, power = x87.parts(value)
mantissa = abs(mantissa)
exponent = ((mantissa.bit_length() - 1 + power) * 19728 >> 16) + 1
scaled = unit.round_integral(_power_of_ten(library, x87.real(mantissa, power), 18 - exponent))
if unit.compare(scaled, 1e18) != -1:
scaled = unit.divide(scaled, 10.0)
exponent += 1
digits = unit.integer(scaled)
if digits >= 10 ** 18:
raise Undefined('Delphi stores a value of 19 decimal digits, which the x87 cannot store as 18.')
return F'{digits:018d}', exponent
def _rounded(negative: bool, text: str, exponent: int, keep: int, decimals: int | None) -> Decimal:
"""
The stored digits rounded half up to the given number of significant digits and to the given
number of digits after the decimal point, without trailing zeros.
"""
if decimals is not None:
keep = min(keep, exponent + decimals)
if keep < 0:
return Decimal(negative, '', 0)
if keep < len(text):
rounded = int(text[:keep] or '0')
if text[keep] >= '5':
rounded += 1
if keep == 0:
if rounded:
return Decimal(negative, '1', exponent + 1)
return Decimal(negative, '', 0)
if rounded >= 10 ** keep:
exponent += 1
text = str(rounded).zfill(keep)
text = text.rstrip('0')
if not text:
return Decimal(negative, '', 0)
return Decimal(negative, text, exponent)
def _scaled(mantissa: int, power: int, scale: int) -> tuple[int, int]:
"""
The numerator and denominator of `mantissa * 2 ** power * 10 ** scale`.
"""
numerator, denominator = mantissa, 1
if power >= 0:
numerator <<= power
else:
denominator <<= -power
if scale >= 0:
numerator *= 10 ** scale
else:
denominator *= 10 ** -scale
return numerator, denominator
def _decimal_exponent(mantissa: int, power: int) -> int:
"""
The exponent e with `10 ** (e - 1) <= mantissa * 2 ** power < 10 ** e` for a positive mantissa.
"""
e = math.floor((mantissa.bit_length() + power - 1) * _LOG10_2) + 1
while True:
numerator, denominator = _scaled(mantissa, power, 1 - e)
if numerator >= denominator:
break
e -= 1
while True:
numerator, denominator = _scaled(mantissa, power, -e)
if numerator < denominator:
break
e += 1
return e
_LOG10_2 = math.log10(2)
_EXTENDED_DENORMAL = -16382
class FloatFormat(enum.Enum):
"""
The formats of Delphi's `FloatToText`.
"""
General = 'general'
Exponent = 'exponent'
Fixed = 'fixed'
Number = 'number'
Currency = 'currency'
_POSITIVE_CURRENCY = (
'{c}{n}',
'{n}{c}',
'{c} {n}',
'{n} {c}',
)
_NEGATIVE_CURRENCY = (
'({c}{n})',
'-{c}{n}',
'{c}-{n}',
'{c}{n}-',
'({n}{c})',
'-{n}{c}',
'{n}-{c}',
'{n}{c}-',
'-{n} {c}',
'-{c} {n}',
'{n} {c}-',
'{c} {n}-',
'{c} -{n}',
'{n}- {c}',
'({c} {n})',
'({n} {c})',
)
def float_to_text(
value: Real,
style: FloatFormat,
precision: int,
digits: int,
runtime: Runtime,
) -> str:
"""
Delphi's `FloatToText`: the general format writes the shortest text of at most `precision`
significant digits and switches to scientific notation with at least `digits` exponent digits
for large and small values; the exponent format writes `precision` significant digits and an
exponent of at least `digits` digits; the fixed, number and currency formats write `digits`
decimals and use the general format for values with more than `precision` integer digits. The
number and currency formats separate thousands, and the currency format places the currency
string as the settings say.
"""
settings = runtime.settings
library = runtime.library
if style is FloatFormat.General or style is FloatFormat.Exponent:
precision = min(precision, library.digits)
rounded = decimal(value, precision, library=library)
else:
rounded = decimal(value, 18, library=library)
if isinstance(rounded, Decimal) and rounded.exponent > min(precision, library.digits):
return float_to_text(value, FloatFormat.General, precision, digits, runtime)
rounded = decimal(value, 18, digits, library=library)
if isinstance(rounded, Special):
return rounded.value
point = settings.decimal_separator
if style is FloatFormat.General:
if not rounded.digits:
return '0'
sign = '-' if rounded.negative else ''
exponent = rounded.exponent
if exponent > precision or exponent < -3:
mantissa = rounded.digits[0]
if len(rounded.digits) > 1:
mantissa = F'{mantissa}{point}{rounded.digits[1:]}'
e = exponent - 1
return F'{sign}{mantissa}E{"-" if e < 0 else ""}{abs(e):0{digits}d}'
return F'{sign}{_fixed(rounded, None, point)}'
if style is FloatFormat.Exponent:
mantissa = rounded.digits.ljust(precision, '0')
e = rounded.exponent - 1 if rounded.digits else 0
sign = '-' if rounded.negative and rounded.digits else ''
if precision > 1:
mantissa = F'{mantissa[0]}{point}{mantissa[1:]}'
return F'{sign}{mantissa}E{"-" if e < 0 else "+"}{abs(e):0{digits}d}'
thousands = '' if style is FloatFormat.Fixed else settings.thousand_separator
number = _fixed(rounded, digits, point, thousands)
negative = rounded.negative and bool(rounded.digits)
if style is FloatFormat.Currency:
if negative:
return _NEGATIVE_CURRENCY[settings.negative_currency_format].format(c=settings.currency_string, n=number)
return _POSITIVE_CURRENCY[settings.currency_format].format(c=settings.currency_string, n=number)
return F'-{number}' if negative else number
def _fixed(rounded: Decimal, decimals: int | None, point: str, thousands: str = '') -> str:
"""
The digits of a decimal as a number without exponent: with the given number of decimals, or
with as many as the digits need.
"""
digits = rounded.digits
exponent = rounded.exponent
if exponent > 0:
whole = digits[:exponent].ljust(exponent, '0')
fraction = digits[exponent:]
else:
whole = '0'
fraction = '0' * -exponent + digits
if thousands:
groups = []
while len(whole) > 3:
groups.append(whole[-3:])
whole = whole[:-3]
groups.append(whole)
whole = thousands.join(reversed(groups))
if decimals is not None:
fraction = fraction[:decimals].ljust(decimals, '0')
if fraction:
return F'{whole}{point}{fraction}'
return whole
def float_to_str(value: Real, runtime: Runtime) -> str:
"""
SysUtils' `FloatToStr`: the general format with 15 significant digits.
"""
return float_to_text(value, FloatFormat.General, 15, 0, runtime)
def str_fixed(value: Real, decimals: int, library: Library) -> str:
"""
Delphi's `Str(Value:0:Decimals)`, which Pascal Script's `FloatToStr` calls with 12 decimals. It
writes the value with a point and the given number of decimals, and a value below 2**-116 or
from 2**119 on in scientific notation with two significant digits and a four-digit exponent;
those digits start after a leading zero of the stored digits.
"""
if isinstance(value, float):
if math.isnan(value):
return 'Nan'
if math.isinf(value):
return '-Inf' if value < 0 else '+Inf'
magnitude = abs(x87.exact(value))
if magnitude != 0 and not _STR_FIXED_RANGE[0] <= magnitude < _STR_FIXED_RANGE[1]:
text, exponent = _stored_digits(value, library)
if text.startswith('0'):
text, exponent = text[1:], exponent - 1
rounded = _rounded(x87.is_negative(value), text, exponent, 2, None)
mantissa = rounded.digits.ljust(2, '0')
e = rounded.exponent - 1
sign = '-' if rounded.negative else ' '
return F'{sign}{mantissa[0]}.{mantissa[1]}E{"-" if e < 0 else "+"}{abs(e):04d}'
rounded = decimal(value, 18, decimals, library=library, denormal_is_zero=False)
assert isinstance(rounded, Decimal)
number = _fixed(rounded, decimals, '.')
return F'-{number}' if rounded.negative and magnitude != 0 else number
_STR_FIXED_RANGE = (Fraction(1, 1 << 116), Fraction(1 << 119))
def val_float(text: str, library: Library) -> tuple[Real | None, int]:
"""
Delphi's `Val` for a float: the value that the text spells and the error code, which is 0 on
success and otherwise the position of the first character that is not part of the number,
counting from 1. Leading spaces are skipped, a sign may follow, then digits with an optional
point and an optional exponent with at least one digit. The digits may be missing, which reads
as zero, but the text may not be empty after the spaces. The library's unit computes the value:
it takes the digits one by one, multiplying by ten and adding the digit, and scales the result
by ten to the exponent minus the number of decimals. The exponent is read as a 32-bit integer
that wraps around; when the exponent and the number of decimals add up to more than the
library's limit, the value is an infinity, whatever the digits.
"""
match = _VAL_FLOAT.match(text)
assert match is not None
spaces, sign, whole, fraction, exponent_mark, exponent_sign, exponent = match.groups()
end = match.end()
if len(spaces) == len(text):
return None, len(text) + 1
if exponent_mark and not exponent:
return None, end + 1
if end < len(text):
return None, end + 1
fraction = fraction or ''
fpu = library.unit
value: Real = 0.0
for digit in (whole + fraction).lstrip('0'):
value = fpu.add(fpu.multiply(value, 10.0), _DIGIT_VALUES[digit])
if x87.is_special(value):
break
power = -len(fraction)
if exponent:
exponent_value = _wrapped_exponent(exponent_sign, exponent)
limit = library.exponent_limit
if limit is not None and exponent_value + len(fraction) > limit:
value = math.inf
power = _signed32(power + exponent_value)
if power == -0x80000000 and library.minimum_power_is_infinite:
value = math.inf
else:
value = _power_of_ten(library, value, power)
return (fpu.negate(value) if sign == '-' else value), 0
_VAL_FLOAT = re.compile(r'( *)([+-]?)([0-9]*)(?:\.([0-9]*))?(?:([eE])([+-]?)([0-9]*))?')
_DIGIT_VALUES = {str(k): float(k) for k in range(10)}
_POWER_LIMIT = 5120
EXPONENT_LIMIT = 5197
"""
The largest sum of the exponent and the number of decimals that 32-bit Delphi's `Val` scales by;
this bound fits every probe of the oracle (exponents from 4932 to 6000 with up to 5201 decimals).
"""
def _signed32(value: int) -> int:
value &= 0xFFFFFFFF
return value - (1 << 32) if value >> 31 else value
def _wrapped_exponent(sign: str, digits: str) -> int:
"""
The exponent that the digits spell as a 32-bit integer, negated for a minus sign, both with
wraparound; only the last 32 digits decide it, since `10 ** 32` is a multiple of `2 ** 32`.
"""
value = int(digits[-32:]) & 0xFFFFFFFF
if sign == '-':
value = -value & 0xFFFFFFFF
return value - (1 << 32) if value >> 31 else value
def _bounded_exponent(sign: str, digits: str, limit: int) -> int:
"""
The exponent that the digits spell, or the given limit with the sign when it is larger.
"""
digits = digits.lstrip('0')
exponent = int(digits) if len(digits) <= len(str(limit)) else limit
exponent = min(exponent, limit)
return -exponent if sign == '-' else exponent
def _table(power: int) -> Real:
return _NEAREST.rational(10 ** power, 1, x87.Format.Extended)
_NEAREST = FPU(x87.MASKED_CONTROL_WORD)
_POWERS = [_table(k) for k in range(32)]
_POWERS_32 = [_table(32 * k) for k in range(16)]
_POWERS_512 = [_table(512 * k) for k in range(10)]
def _power_of_ten(library: Library, value: Real, power: int) -> Real:
"""
The value times ten to the given power, as Delphi's `FPower10` computes it: it multiplies by a
power of ten, or divides by it for a negative power, as the product of three powers from a table
of Extended values: `10 ** a`, `10 ** (32 * b)`, and `10 ** (512 * c)`. When `b` is beyond the
end of the library's table, the library scales by two of its powers. A power of 5120 or more
gives an infinity, and one of -5120 or less zero. The power -2**31, which Delphi cannot negate,
reads a power after the end of the table, which is `Undefined`.
"""
if power == 0:
return value
if power == -0x80000000:
raise Undefined('Delphi scales by a power of ten from after the end of its table.')
negative = power < 0
power = abs(power)
if power >= _POWER_LIMIT:
return 0.0 if negative else math.inf
fpu = library.unit
scale = fpu.divide if negative else fpu.multiply
value = scale(value, _POWERS[power & 31])
if b := (power >> 5) & 15:
end = library.table_end
if b > end:
value = scale(value, _POWERS_32[b - end])
b = end
value = scale(value, _POWERS_32[b])
if c := power >> 9:
value = scale(value, _POWERS_512[c])
return value
class OleNumber(NamedTuple):
"""
A number that Windows read from a text, and whether the text wrote it with an exponent.
"""
value: Fraction
exponent: bool
def ole_number(text: str, settings: FormatSettings) -> OleNumber | None:
"""
The number that Windows reads from a text when Delphi converts a Variant that holds a string,
with the regional settings: white space around it; `&H` before hexadecimal and `&O` before
octal digits; otherwise a sign before or after the number, or parentheses around it for a
negative number, the currency string before or after it, digits among which the thousand
separator may stand, the decimal separator and more digits, and an exponent. Returns `None` for
any other text. The value is exact, except that a value of more than 10**400 in magnitude
becomes 10**400 and a value of less than 10**-400 becomes 10**-400, with its sign, and that the
digits after the first 800 significant ones only count as a small amount beyond them; no format
that Windows converts to tells these values apart.
"""
text = text.strip(_OLE_WHITESPACE)
prefix = text[:2].upper()
if prefix in ('&H', '&O'):
digits = text[2:]
base = 16 if prefix == '&H' else 8
if not digits or not digits.isalnum():
return None
digits = digits.lstrip('0') or '0'
if len(digits) > _OLE_DIGITS:
return OleNumber(Fraction(10) ** _OLE_EXPONENT, False)
try:
return OleNumber(Fraction(int(digits, base)), False)
except ValueError:
return None
negative = False
if text.startswith('(') and text.endswith(')'):
negative = True
text = text[1:-1].strip(_OLE_WHITESPACE)
currency = settings.currency_string
sign = None
if text[:1] in ('+', '-'):
sign = text[0]
text = text[1:].strip(_OLE_WHITESPACE)
if currency and text.startswith(currency):
text = text[len(currency):].strip(_OLE_WHITESPACE)
elif currency and text.endswith(currency):
text = text[:-len(currency)].strip(_OLE_WHITESPACE)
if sign is None and text[:1] in ('+', '-'):
sign = text[0]
text = text[1:].strip(_OLE_WHITESPACE)
if sign is None and text[-1:] in ('+', '-'):
sign = text[-1]
text = text[:-1].strip(_OLE_WHITESPACE)
if sign is not None and negative:
return None
number = _ole_magnitude(text, settings)
if number is None:
return None
if negative or sign == '-':
return OleNumber(-number.value, number.exponent)
return number
_OLE_WHITESPACE = ' \t'
_OLE_DIGITS = 800
_OLE_EXPONENT = 400
def _ole_magnitude(text: str, settings: FormatSettings) -> OleNumber | None:
thousands = settings.thousand_separator
point = settings.decimal_separator
pattern = F'([0-9](?:[0-9]|{re.escape(thousands)})*)?(?:{re.escape(point)}([0-9]*))?(?:[eE]([+-]?)([0-9]*))?'
match = re.fullmatch(pattern, text)
if match is None:
return None
whole, fraction, exponent_sign, exponent = match.groups()
whole = whole or ''
if thousands:
whole = whole.replace(thousands, '')
fraction = fraction or ''
if not whole and not fraction:
return None
if exponent is not None and not exponent:
return None
written = exponent is not None
digits = (whole + fraction).lstrip('0')
if not digits:
return OleNumber(Fraction(0), written)
scale = -len(fraction)
if exponent:
scale += _bounded_exponent(exponent_sign, exponent, 2 * _OLE_EXPONENT + len(whole) + len(fraction))
if len(digits) > _OLE_DIGITS:
sticky = digits[_OLE_DIGITS:].strip('0') != ''
scale += len(digits) - _OLE_DIGITS
digits = digits[:_OLE_DIGITS]
if sticky:
digits += '1'
scale -= 1
top = len(digits) + scale
if top > _OLE_EXPONENT:
return OleNumber(Fraction(10) ** _OLE_EXPONENT, written)
if top < -_OLE_EXPONENT:
return OleNumber(Fraction(1, 10 ** _OLE_EXPONENT), written)
return OleNumber(Fraction(int(digits)) * Fraction(10) ** scale, written)
def shortest(value: Real) -> str:
"""
The shortest text that names the value exactly, in the style of Python's text of a float: for
an `refinery.lib.inno.delphi.x87.Extended`, the fewest significant digits whose nearest Extended
is the value.
"""
if not isinstance(value, x87.Extended):
return repr(value)
negative = value.mantissa < 0
mantissa = abs(value.mantissa)
power = value.exponent
exponent = _decimal_exponent(mantissa, power)
target = x87.Extended(mantissa, power)
for count in range(1, 22):
numerator, denominator = _scaled(mantissa, power, count - exponent)
digits, remainder = divmod(numerator, denominator)
if 2 * remainder > denominator or 2 * remainder == denominator and digits & 1:
digits += 1
scale = exponent - count
if scale >= 0:
back = _NEAREST.rational(digits * 10 ** scale, 1, x87.Format.Extended)
else:
back = _NEAREST.rational(digits, 10 ** -scale, x87.Format.Extended)
if back == target:
break
else:
raise ValueError(value)
text = str(digits)
point = scale + len(text)
text = text.rstrip('0')
if -4 < point <= 16:
if point <= 0:
number = F'0.{"0" * -point}{text}'
elif point >= len(text):
number = F'{text}{"0" * (point - len(text))}.0'
else:
number = F'{text[:point]}.{text[point:]}'
else:
mantissa_text = text[0] if len(text) == 1 else F'{text[0]}.{text[1:]}'
e = point - 1
number = F'{mantissa_text}e{"-" if e < 0 else "+"}{abs(e):02d}'
return F'-{number}' if negative else number
Global variables
var EXPONENT_LIMIT-
The largest sum of the exponent and the number of decimals that 32-bit Delphi's
Valscales by; this bound fits every probe of the oracle (exponents from 4932 to 6000 with up to 5201 decimals).
Functions
def val_integer(text, bits)-
Delphi's
Valfor an integer of the given width: the value and the error code, which is 0 on success and otherwise the position of the first character that is not part of the number, counting from 1. Leading spaces are skipped; a sign may follow;$,x,X,0xand0Xstart a hexadecimal number. A hexadecimal number may fill all bits, a decimal number must fit the signed range.Expand source code Browse git
def val_integer(text: str, bits: int) -> tuple[int, int]: """ Delphi's `Val` for an integer of the given width: the value and the error code, which is 0 on success and otherwise the position of the first character that is not part of the number, counting from 1. Leading spaces are skipped; a sign may follow; `$`, `x`, `X`, `0x` and `0X` start a hexadecimal number. A hexadecimal number may fill all bits, a decimal number must fit the signed range. """ limit = (1 << (bits - 1)) - 1 wrap = 1 << bits def at(position: int) -> str: if 1 <= position <= len(text): return text[position - 1] return '\0' def signed(value: int) -> int: value %= wrap return value - wrap if value > limit else value if not text: return 0, 1 position = 1 while at(position) == ' ': position += 1 negative = False if at(position) == '-': negative = True position += 1 elif at(position) == '+': position += 1 result = 0 empty = True first = at(position) if first == '$' or first in 'xX' or first == '0' and position < len(text) and at(position + 1) in 'xX': if first == '0': position += 1 position += 1 while (digit := _HEX_DIGITS.get(at(position))) is not None: if result < 0 or result > limit >> 3: break result = signed((result << 4) + digit) position += 1 empty = False if negative: result = signed(-result) else: while (digit := _DECIMAL_DIGITS.get(at(position))) is not None: if result < 0 or result > limit // 10: break result = signed(result * 10 + digit) position += 1 empty = False if negative: result = signed(-result) if result != 0 and negative != (result < 0): position -= 1 if at(position) != '\0' or empty: return result, position return result, 0 def val_unsigned(text)-
Delphi's
Valfor a UInt64: as for an Int64, but a decimal number may fill all 64 bits and only zero may have a minus sign.Expand source code Browse git
def val_unsigned(text: str) -> tuple[int, int]: """ Delphi's `Val` for a UInt64: as for an Int64, but a decimal number may fill all 64 bits and only zero may have a minus sign. """ stripped = text.lstrip(' ') negative = stripped.startswith('-') if negative: stripped = stripped[1:] elif stripped.startswith('+'): stripped = stripped[1:] offset = len(text) - len(stripped) if not stripped: return 0, offset + 1 if text else 1 hexadecimal = stripped[0] in '$xX' or stripped[:2] in ('0x', '0X') body = stripped[2:] if stripped[:2] in ('0x', '0X') else stripped[1:] if hexadecimal else stripped head = len(stripped) - len(body) digits = _HEX_DIGITS if hexadecimal else _DECIMAL_DIGITS base = 16 if hexadecimal else 10 value = 0 for k, character in enumerate(body): digit = digits.get(character) if digit is None: return value, offset + head + k + 1 value = value * base + digit if value >> 64: return 0, offset + head + k + 1 if not body: return 0, offset + head + 1 if negative and value: return 0, 1 return value, 0 def str_to_int64(text)-
SysUtils'
StrToInt64: raises anEConvertErrorfor a text thatValdoes not read as an Int64.Expand source code Browse git
def str_to_int64(text: str) -> int: """ SysUtils' `StrToInt64`: raises an `EConvertError` for a text that `Val` does not read as an Int64. """ value, error = val_integer(text, 64) if error: raise EConvertError(SysConst.SInvalidInteger % text) return value def str_to_int64_default(text, default)-
SysUtils'
StrToInt64Def: the default for a text thatValdoes not read as an Int64.Expand source code Browse git
def str_to_int64_default(text: str, default: int) -> int: """ SysUtils' `StrToInt64Def`: the default for a text that `Val` does not read as an Int64. """ value, error = val_integer(text, 64) return default if error else value def str_to_uint64(text)-
SysUtils'
StrToUInt64: raises anEConvertErrorfor a text thatValdoes not read as a UInt64.Expand source code Browse git
def str_to_uint64(text: str) -> int: """ SysUtils' `StrToUInt64`: raises an `EConvertError` for a text that `Val` does not read as a UInt64. """ value, error = val_unsigned(text) if error: raise EConvertError(SysConst.SInvalidInteger % text) return value def str_to_uint64_default(text, default)-
SysUtils'
StrToUInt64Def: the default for a text thatValdoes not read as a UInt64.Expand source code Browse git
def str_to_uint64_default(text: str, default: int) -> int: """ SysUtils' `StrToUInt64Def`: the default for a text that `Val` does not read as a UInt64. """ value, error = val_unsigned(text) return default if error else value def decimal(value, precision, decimals=None, *, library, denormal_is_zero=True)-
Delphi's
FloatToDecimal: the digits of the value rounded to the given number of significant digits, at most those of the library, and to the given number of digits after the decimal point. The digits come from_stored_digitsand are rounded half up. Trailing zeros are removed; zero has no digits, and so has a denormal Extended unless the caller isStr, which writes its digits.Expand source code Browse git
def decimal( value: Real, precision: int, decimals: int | None = None, *, library: Library, denormal_is_zero: bool = True, ) -> Decimal | Special: """ Delphi's `FloatToDecimal`: the digits of the value rounded to the given number of significant digits, at most those of the library, and to the given number of digits after the decimal point. The digits come from `_stored_digits` and are rounded half up. Trailing zeros are removed; zero has no digits, and so has a denormal Extended unless the caller is `Str`, which writes its digits. """ if isinstance(value, float): if math.isnan(value): return Special.NaN if math.isinf(value): return Special.NegativeInfinity if value < 0 else Special.Infinity negative = x87.is_negative(value) if value == 0: return Decimal(negative, '', 0) mantissa, power = x87.parts(value) if denormal_is_zero and abs(mantissa).bit_length() + power <= _EXTENDED_DENORMAL: return Decimal(negative, '', 0) text, exponent = _stored_digits(value, library) return _rounded(negative, text, exponent, min(precision, library.digits), decimals) def float_to_text(value, style, precision, digits, runtime)-
Delphi's
FloatToText: the general format writes the shortest text of at mostprecisionsignificant digits and switches to scientific notation with at leastdigitsexponent digits for large and small values; the exponent format writesprecisionsignificant digits and an exponent of at leastdigitsdigits; the fixed, number and currency formats writedigitsdecimals and use the general format for values with more thanprecisioninteger digits. The number and currency formats separate thousands, and the currency format places the currency string as the settings say.Expand source code Browse git
def float_to_text( value: Real, style: FloatFormat, precision: int, digits: int, runtime: Runtime, ) -> str: """ Delphi's `FloatToText`: the general format writes the shortest text of at most `precision` significant digits and switches to scientific notation with at least `digits` exponent digits for large and small values; the exponent format writes `precision` significant digits and an exponent of at least `digits` digits; the fixed, number and currency formats write `digits` decimals and use the general format for values with more than `precision` integer digits. The number and currency formats separate thousands, and the currency format places the currency string as the settings say. """ settings = runtime.settings library = runtime.library if style is FloatFormat.General or style is FloatFormat.Exponent: precision = min(precision, library.digits) rounded = decimal(value, precision, library=library) else: rounded = decimal(value, 18, library=library) if isinstance(rounded, Decimal) and rounded.exponent > min(precision, library.digits): return float_to_text(value, FloatFormat.General, precision, digits, runtime) rounded = decimal(value, 18, digits, library=library) if isinstance(rounded, Special): return rounded.value point = settings.decimal_separator if style is FloatFormat.General: if not rounded.digits: return '0' sign = '-' if rounded.negative else '' exponent = rounded.exponent if exponent > precision or exponent < -3: mantissa = rounded.digits[0] if len(rounded.digits) > 1: mantissa = F'{mantissa}{point}{rounded.digits[1:]}' e = exponent - 1 return F'{sign}{mantissa}E{"-" if e < 0 else ""}{abs(e):0{digits}d}' return F'{sign}{_fixed(rounded, None, point)}' if style is FloatFormat.Exponent: mantissa = rounded.digits.ljust(precision, '0') e = rounded.exponent - 1 if rounded.digits else 0 sign = '-' if rounded.negative and rounded.digits else '' if precision > 1: mantissa = F'{mantissa[0]}{point}{mantissa[1:]}' return F'{sign}{mantissa}E{"-" if e < 0 else "+"}{abs(e):0{digits}d}' thousands = '' if style is FloatFormat.Fixed else settings.thousand_separator number = _fixed(rounded, digits, point, thousands) negative = rounded.negative and bool(rounded.digits) if style is FloatFormat.Currency: if negative: return _NEGATIVE_CURRENCY[settings.negative_currency_format].format(c=settings.currency_string, n=number) return _POSITIVE_CURRENCY[settings.currency_format].format(c=settings.currency_string, n=number) return F'-{number}' if negative else number def float_to_str(value, runtime)-
SysUtils'
FloatToStr: the general format with 15 significant digits.Expand source code Browse git
def float_to_str(value: Real, runtime: Runtime) -> str: """ SysUtils' `FloatToStr`: the general format with 15 significant digits. """ return float_to_text(value, FloatFormat.General, 15, 0, runtime) def str_fixed(value, decimals, library)-
Delphi's
Str(Value:0:Decimals), which Pascal Script'sFloatToStrcalls with 12 decimals. It writes the value with a point and the given number of decimals, and a value below 2-116 or from 2119 on in scientific notation with two significant digits and a four-digit exponent; those digits start after a leading zero of the stored digits.Expand source code Browse git
def str_fixed(value: Real, decimals: int, library: Library) -> str: """ Delphi's `Str(Value:0:Decimals)`, which Pascal Script's `FloatToStr` calls with 12 decimals. It writes the value with a point and the given number of decimals, and a value below 2**-116 or from 2**119 on in scientific notation with two significant digits and a four-digit exponent; those digits start after a leading zero of the stored digits. """ if isinstance(value, float): if math.isnan(value): return 'Nan' if math.isinf(value): return '-Inf' if value < 0 else '+Inf' magnitude = abs(x87.exact(value)) if magnitude != 0 and not _STR_FIXED_RANGE[0] <= magnitude < _STR_FIXED_RANGE[1]: text, exponent = _stored_digits(value, library) if text.startswith('0'): text, exponent = text[1:], exponent - 1 rounded = _rounded(x87.is_negative(value), text, exponent, 2, None) mantissa = rounded.digits.ljust(2, '0') e = rounded.exponent - 1 sign = '-' if rounded.negative else ' ' return F'{sign}{mantissa[0]}.{mantissa[1]}E{"-" if e < 0 else "+"}{abs(e):04d}' rounded = decimal(value, 18, decimals, library=library, denormal_is_zero=False) assert isinstance(rounded, Decimal) number = _fixed(rounded, decimals, '.') return F'-{number}' if rounded.negative and magnitude != 0 else number def val_float(text, library)-
Delphi's
Valfor a float: the value that the text spells and the error code, which is 0 on success and otherwise the position of the first character that is not part of the number, counting from 1. Leading spaces are skipped, a sign may follow, then digits with an optional point and an optional exponent with at least one digit. The digits may be missing, which reads as zero, but the text may not be empty after the spaces. The library's unit computes the value: it takes the digits one by one, multiplying by ten and adding the digit, and scales the result by ten to the exponent minus the number of decimals. The exponent is read as a 32-bit integer that wraps around; when the exponent and the number of decimals add up to more than the library's limit, the value is an infinity, whatever the digits.Expand source code Browse git
def val_float(text: str, library: Library) -> tuple[Real | None, int]: """ Delphi's `Val` for a float: the value that the text spells and the error code, which is 0 on success and otherwise the position of the first character that is not part of the number, counting from 1. Leading spaces are skipped, a sign may follow, then digits with an optional point and an optional exponent with at least one digit. The digits may be missing, which reads as zero, but the text may not be empty after the spaces. The library's unit computes the value: it takes the digits one by one, multiplying by ten and adding the digit, and scales the result by ten to the exponent minus the number of decimals. The exponent is read as a 32-bit integer that wraps around; when the exponent and the number of decimals add up to more than the library's limit, the value is an infinity, whatever the digits. """ match = _VAL_FLOAT.match(text) assert match is not None spaces, sign, whole, fraction, exponent_mark, exponent_sign, exponent = match.groups() end = match.end() if len(spaces) == len(text): return None, len(text) + 1 if exponent_mark and not exponent: return None, end + 1 if end < len(text): return None, end + 1 fraction = fraction or '' fpu = library.unit value: Real = 0.0 for digit in (whole + fraction).lstrip('0'): value = fpu.add(fpu.multiply(value, 10.0), _DIGIT_VALUES[digit]) if x87.is_special(value): break power = -len(fraction) if exponent: exponent_value = _wrapped_exponent(exponent_sign, exponent) limit = library.exponent_limit if limit is not None and exponent_value + len(fraction) > limit: value = math.inf power = _signed32(power + exponent_value) if power == -0x80000000 and library.minimum_power_is_infinite: value = math.inf else: value = _power_of_ten(library, value, power) return (fpu.negate(value) if sign == '-' else value), 0 def ole_number(text, settings)-
The number that Windows reads from a text when Delphi converts a Variant that holds a string, with the regional settings: white space around it;
&Hbefore hexadecimal and&Obefore octal digits; otherwise a sign before or after the number, or parentheses around it for a negative number, the currency string before or after it, digits among which the thousand separator may stand, the decimal separator and more digits, and an exponent. ReturnsNonefor any other text. The value is exact, except that a value of more than 10400 in magnitude becomes 10400 and a value of less than 10-400 becomes 10-400, with its sign, and that the digits after the first 800 significant ones only count as a small amount beyond them; no format that Windows converts to tells these values apart.Expand source code Browse git
def ole_number(text: str, settings: FormatSettings) -> OleNumber | None: """ The number that Windows reads from a text when Delphi converts a Variant that holds a string, with the regional settings: white space around it; `&H` before hexadecimal and `&O` before octal digits; otherwise a sign before or after the number, or parentheses around it for a negative number, the currency string before or after it, digits among which the thousand separator may stand, the decimal separator and more digits, and an exponent. Returns `None` for any other text. The value is exact, except that a value of more than 10**400 in magnitude becomes 10**400 and a value of less than 10**-400 becomes 10**-400, with its sign, and that the digits after the first 800 significant ones only count as a small amount beyond them; no format that Windows converts to tells these values apart. """ text = text.strip(_OLE_WHITESPACE) prefix = text[:2].upper() if prefix in ('&H', '&O'): digits = text[2:] base = 16 if prefix == '&H' else 8 if not digits or not digits.isalnum(): return None digits = digits.lstrip('0') or '0' if len(digits) > _OLE_DIGITS: return OleNumber(Fraction(10) ** _OLE_EXPONENT, False) try: return OleNumber(Fraction(int(digits, base)), False) except ValueError: return None negative = False if text.startswith('(') and text.endswith(')'): negative = True text = text[1:-1].strip(_OLE_WHITESPACE) currency = settings.currency_string sign = None if text[:1] in ('+', '-'): sign = text[0] text = text[1:].strip(_OLE_WHITESPACE) if currency and text.startswith(currency): text = text[len(currency):].strip(_OLE_WHITESPACE) elif currency and text.endswith(currency): text = text[:-len(currency)].strip(_OLE_WHITESPACE) if sign is None and text[:1] in ('+', '-'): sign = text[0] text = text[1:].strip(_OLE_WHITESPACE) if sign is None and text[-1:] in ('+', '-'): sign = text[-1] text = text[:-1].strip(_OLE_WHITESPACE) if sign is not None and negative: return None number = _ole_magnitude(text, settings) if number is None: return None if negative or sign == '-': return OleNumber(-number.value, number.exponent) return number def shortest(value)-
The shortest text that names the value exactly, in the style of Python's text of a float: for an
Extended, the fewest significant digits whose nearest Extended is the value.Expand source code Browse git
def shortest(value: Real) -> str: """ The shortest text that names the value exactly, in the style of Python's text of a float: for an `refinery.lib.inno.delphi.x87.Extended`, the fewest significant digits whose nearest Extended is the value. """ if not isinstance(value, x87.Extended): return repr(value) negative = value.mantissa < 0 mantissa = abs(value.mantissa) power = value.exponent exponent = _decimal_exponent(mantissa, power) target = x87.Extended(mantissa, power) for count in range(1, 22): numerator, denominator = _scaled(mantissa, power, count - exponent) digits, remainder = divmod(numerator, denominator) if 2 * remainder > denominator or 2 * remainder == denominator and digits & 1: digits += 1 scale = exponent - count if scale >= 0: back = _NEAREST.rational(digits * 10 ** scale, 1, x87.Format.Extended) else: back = _NEAREST.rational(digits, 10 ** -scale, x87.Format.Extended) if back == target: break else: raise ValueError(value) text = str(digits) point = scale + len(text) text = text.rstrip('0') if -4 < point <= 16: if point <= 0: number = F'0.{"0" * -point}{text}' elif point >= len(text): number = F'{text}{"0" * (point - len(text))}.0' else: number = F'{text[:point]}.{text[point:]}' else: mantissa_text = text[0] if len(text) == 1 else F'{text[0]}.{text[1:]}' e = point - 1 number = F'{mantissa_text}e{"-" if e < 0 else "+"}{abs(e):02d}' return F'-{number}' if negative else number
Classes
class FormatSettings (decimal_separator='.', thousand_separator=',', currency_string='$', currency_format=0, negative_currency_format=0, currency_decimals=2)-
The regional settings that Delphi formats numbers with, named as the fields of Delphi's record
TFormatSettings. The defaults are those of English (United States).Expand source code Browse git
@dataclass(frozen=True) class FormatSettings: """ The regional settings that Delphi formats numbers with, named as the fields of Delphi's record `TFormatSettings`. The defaults are those of English (United States). """ decimal_separator: str = '.' thousand_separator: str = ',' currency_string: str = '$' currency_format: int = 0 negative_currency_format: int = 0 currency_decimals: int = 2 @classmethod def of_locale(cls, lcid: int) -> FormatSettings: """ The regional settings that Windows 11 has for the locale with the given ID, as Delphi reads them: the first character of each separator. """ try: _, *fields = _regional_formats()[F'{lcid:04x}'] except KeyError: raise ValueError(F'Windows has no regional format for the locale 0x{lcid:04X}.') from None return cls(*fields)Static methods
def of_locale(lcid)-
The regional settings that Windows 11 has for the locale with the given ID, as Delphi reads them: the first character of each separator.
Instance variables
var decimal_separator-
The type of the None singleton.
var thousand_separator-
The type of the None singleton.
var currency_string-
The type of the None singleton.
var currency_format-
The type of the None singleton.
var negative_currency_format-
The type of the None singleton.
var currency_decimals-
The type of the None singleton.
class Special (*args, **kwds)-
The values of a float that have no digits.
Expand source code Browse git
class Special(enum.Enum): """ The values of a float that have no digits. """ Infinity = 'INF' NegativeInfinity = '-INF' NaN = 'NAN'Ancestors
- enum.Enum
Class variables
var Infinity-
The type of the None singleton.
var NegativeInfinity-
The type of the None singleton.
var NaN-
The type of the None singleton.
class Library (unit, digits, table_end, exponent_limit, minimum_power_is_infinite)-
The routines of Delphi's run-time library that convert between floats and text, as Delphi builds them for a platform. They compute on
unitand write at mostdigitssignificant digits. They scale by powers of ten from a table whose powers10 ** (32 * k)end atk = table_end.Valgives an infinity when the exponent and the number of decimals add up to more thanexponent_limit, if there is a limit. The power-2**31, which the library cannot negate, gives an infinity whenminimum_power_is_infiniteholds; otherwise the library reads a power after the end of its table.Expand source code Browse git
@dataclass(frozen=True) class Library: """ The routines of Delphi's run-time library that convert between floats and text, as Delphi builds them for a platform. They compute on `unit` and write at most `digits` significant digits. They scale by powers of ten from a table whose powers `10 ** (32 * k)` end at `k = table_end`. `Val` gives an infinity when the exponent and the number of decimals add up to more than `exponent_limit`, if there is a limit. The power `-2**31`, which the library cannot negate, gives an infinity when `minimum_power_is_infinite` holds; otherwise the library reads a power after the end of its table. """ unit: FPU digits: int table_end: int exponent_limit: int | None minimum_power_is_infinite: boolInstance variables
var unit-
The type of the None singleton.
var digits-
The type of the None singleton.
var table_end-
The type of the None singleton.
var exponent_limit-
The type of the None singleton.
var minimum_power_is_infinite-
The type of the None singleton.
class Decimal (negative, digits, exponent)-
A float as decimal digits: the value is
0.digits * 10 ** exponent, negative or not. Delphi's recordTFloatRecholds a float in this way.Expand source code Browse git
@dataclass(frozen=True) class Decimal: """ A float as decimal digits: the value is `0.digits * 10 ** exponent`, negative or not. Delphi's record `TFloatRec` holds a float in this way. """ negative: bool digits: str exponent: intInstance variables
var negative-
The type of the None singleton.
var digits-
The type of the None singleton.
var exponent-
The type of the None singleton.
class FloatFormat (*args, **kwds)-
The formats of Delphi's
FloatToText.Expand source code Browse git
class FloatFormat(enum.Enum): """ The formats of Delphi's `FloatToText`. """ General = 'general' Exponent = 'exponent' Fixed = 'fixed' Number = 'number' Currency = 'currency'Ancestors
- enum.Enum
Class variables
var General-
The type of the None singleton.
var Exponent-
The type of the None singleton.
var Fixed-
The type of the None singleton.
var Number-
The type of the None singleton.
var Currency-
The type of the None singleton.
class OleNumber (value, exponent)-
A number that Windows read from a text, and whether the text wrote it with an exponent.
Expand source code Browse git
class OleNumber(NamedTuple): """ A number that Windows read from a text, and whether the text wrote it with an exponent. """ value: Fraction exponent: boolAncestors
- builtins.tuple
Instance variables
var value-
Alias for field number 0
Expand source code Browse git
class OleNumber(NamedTuple): """ A number that Windows read from a text, and whether the text wrote it with an exponent. """ value: Fraction exponent: bool var exponent-
Alias for field number 1
Expand source code Browse git
class OleNumber(NamedTuple): """ A number that Windows read from a text, and whether the text wrote it with an exponent. """ value: Fraction exponent: bool