feat(fstring): implement basic number formatting
This commit is contained in:
@@ -1,9 +1,23 @@
|
|||||||
|
// Basic type
|
||||||
let a = 42
|
let a = 42
|
||||||
print(f"int: {a:d}; hex: {a:x}; HEX: {a:X}; oct: {a:o}; bin: {a:b}")
|
print(f"int: {a:d}; hex: {a:x}; HEX: {a:X}; oct: {a:o}; bin: {a:b}")
|
||||||
|
|
||||||
|
// Grouping
|
||||||
let b = 1234567890
|
let b = 1234567890
|
||||||
print(f"{b:,}")
|
print(f"{b:,}")
|
||||||
print(f"{b:_}")
|
print(f"{b:_}")
|
||||||
|
|
||||||
|
let c = 1234.5678
|
||||||
|
print(f"{c:,._}")
|
||||||
|
print(f"{c:_.,}")
|
||||||
|
|
||||||
|
// Sign
|
||||||
|
|
||||||
|
// Percentage
|
||||||
let pts = 19
|
let pts = 19
|
||||||
let total = 22
|
let total = 22
|
||||||
print(f"Correct answers: {pts/total:.2%}")
|
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.spec import FormatSpec
|
||||||
from src.core.format_spec.token import TokenType, Token
|
from src.core.format_spec.token import TokenType, Token
|
||||||
@@ -21,10 +21,134 @@ class StringFormatter:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@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):
|
if not isinstance(obj, expected_type):
|
||||||
raise PebbleRuntimeError(token, f"Invalid value type. Expected {expected_type}, got {type(obj)}")
|
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:
|
def format(self, obj: Any, spec: FormatSpec) -> str:
|
||||||
# TODO
|
res: str = ""
|
||||||
return str(obj)
|
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