feat(fstring): implement basic number formatting

This commit is contained in:
2026-02-07 23:24:20 +01:00
parent 017df98297
commit 3f0aa44b09
2 changed files with 143 additions and 5 deletions

View File

@@ -1,9 +1,23 @@
// Basic type
let a = 42
print(f"int: {a:d}; hex: {a:x}; HEX: {a:X}; oct: {a:o}; bin: {a:b}")
// Grouping
let b = 1234567890
print(f"{b:,}")
print(f"{b:_}")
let c = 1234.5678
print(f"{c:,._}")
print(f"{c:_.,}")
// Sign
// Percentage
let pts = 19
let total = 22
print(f"Correct answers: {pts/total:.2%}")
print(f"Correct answers: {pts/total:.2%}")
// Precision
// Complex

View File

@@ -1,4 +1,4 @@
from typing import Any
from typing import Any, Optional
from src.core.format_spec.spec import FormatSpec
from src.core.format_spec.token import TokenType, Token
@@ -21,10 +21,134 @@ class StringFormatter:
return obj
@staticmethod
def check_type(token: Token, obj: Any, expected_type: type | tuple[type, ...]):
def assert_type(token: Token, obj: Any, expected_type: type | tuple[type, ...]):
if not isinstance(obj, expected_type):
raise PebbleRuntimeError(token, f"Invalid value type. Expected {expected_type}, got {type(obj)}")
def check_type(self, obj: Any, fmt_type_token: Token) -> Any:
match fmt_type_token.type:
case TokenType.T_STR:
self.assert_type(fmt_type_token, obj, str)
case TokenType.T_BIN | TokenType.T_DEC | TokenType.T_HEX | TokenType.T_HEX_CAPS | TokenType.T_OCT:
if isinstance(obj, float) and obj.is_integer():
obj = int(obj)
self.assert_type(fmt_type_token, obj, int)
case TokenType.T_SCI | TokenType.T_FIX | TokenType.T_PCT:
if isinstance(obj, int):
obj = float(obj)
self.assert_type(fmt_type_token, obj, float)
case _:
raise PebbleRuntimeError(fmt_type_token, f"Unsupported formatting type '{fmt_type_token.lexeme}'.")
return obj
def guess_type(self, obj: Any) -> tuple[Optional[TokenType], Any]:
fmt_type: Optional[TokenType] = None
if isinstance(obj, str):
fmt_type = TokenType.T_STR
elif isinstance(obj, int):
fmt_type = TokenType.T_DEC
elif isinstance(obj, float):
if obj.is_integer():
obj = int(obj)
fmt_type = TokenType.T_DEC
else:
fmt_type = TokenType.T_FIX
return fmt_type, obj
def format(self, obj: Any, spec: FormatSpec) -> str:
# TODO
return str(obj)
res: str = ""
fmt_type: Optional[TokenType]
fmt_type_token: Optional[Token]
if spec.type is not None:
obj = self.check_type(obj, spec.type)
fmt_type = spec.type.type
else:
fmt_type, obj = self.guess_type(obj)
match fmt_type:
case None:
res = self.stringify(obj)
case TokenType.T_STR:
res = obj
case TokenType.T_BIN | TokenType.T_DEC | TokenType.T_HEX | TokenType.T_HEX_CAPS | TokenType.T_OCT:
res = self.format_int(obj, spec, fmt_type)
case TokenType.T_SCI | TokenType.T_FIX | TokenType.T_PCT:
res = self.format_float(obj, spec, fmt_type)
if spec.number.integral.width is not None:
res = self.pad(res, spec.number.integral.width)
return res
def format_int(self, value: int, spec: FormatSpec, fmt_type: TokenType) -> str:
if spec.number.decimal is not None:
raise PebbleRuntimeError(spec.number.decimal.dot, "Incompatible decimal options with int type.")
sign: str = self.make_sign(value, spec)
string: str = self.format_int_part(value, fmt_type, spec.number.integral.grouping)
return sign + string
def format_float(self, value: float, spec: FormatSpec, fmt_type: TokenType) -> str:
sign: str = self.make_sign(value, spec)
value = abs(value)
integer: int = int(value)
decimal: float = value - integer
integer_spec = spec.number.integral
decimal_spec = spec.number.decimal
if decimal_spec is not None:
if decimal_spec.precision is not None:
decimal = round(decimal, decimal_spec.precision)
integer_str: str = self.format_int_part(integer, fmt_type, integer_spec.grouping)
decimal_str: str = self.format_int_part(int(str(decimal)[2:]), fmt_type, None if decimal_spec is None else decimal_spec.grouping)
if decimal_str:
decimal_str = f".{decimal_str}"
return f"{sign}{integer_str}{decimal_str}"
def format_int_part(self, value: int, fmt_type: TokenType, grouping: Optional[Token]):
groups: int = 4
string: str = str(value)
match fmt_type:
case TokenType.T_BIN:
string = f"{value:b}"
case TokenType.T_HEX:
string = f"{value:x}"
case TokenType.T_HEX_CAPS:
string = f"{value:X}"
case TokenType.T_OCT:
string = f"{value:o}"
case _:
groups = 3
if grouping is not None:
string = self.make_groups(string, groups, grouping.lexeme)
return string
def make_sign(self, value: int | float, spec: FormatSpec) -> str:
sign: Optional[Token] = spec.options.sign
plus: str = ""
if sign is not None:
match sign.type:
case TokenType.PLUS:
plus = "+"
case TokenType.SPACE:
plus = " "
return "-" if value < 0 else plus
def make_groups(self, string: str, group_size: int, sep: str) -> str:
groups: list[str] = []
length: int = len(string)
for i in range(0, length, group_size):
groups.append(string[max(0, length - i - group_size):length - i])
return sep.join(reversed(groups))
def pad(self, string: str, width: int) -> str:
to_pad: int = width - len(string)
if to_pad > 0:
string = " " * to_pad + string
return string