feat(fstring): implement basic number formatting
This commit is contained in:
@@ -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%}")
|
||||
|
||||
// Precision
|
||||
|
||||
// Complex
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user