diff --git a/examples/basic/23_format_spec.peb b/examples/basic/23_format_spec.peb index a574c03..765fdd6 100644 --- a/examples/basic/23_format_spec.peb +++ b/examples/basic/23_format_spec.peb @@ -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%}") \ No newline at end of file +print(f"Correct answers: {pts/total:.2%}") + +// Precision + +// Complex \ No newline at end of file diff --git a/src/core/format_spec/string_formatter.py b/src/core/format_spec/string_formatter.py index 0935250..b6b7ea8 100644 --- a/src/core/format_spec/string_formatter.py +++ b/src/core/format_spec/string_formatter.py @@ -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