feat(fstring): add alignment

This commit is contained in:
2026-02-08 01:08:27 +01:00
parent 00162d8d68
commit 2bfde130e8
6 changed files with 99 additions and 11 deletions

View File

@@ -45,4 +45,15 @@ print(f"{d2:e}")
print(f"{0:e}")
print()
print("Alignment")
print(f"{"test":10}")
print(f"{"test":<10}")
print(f"{"test":>10}")
print(f"{"test":^10}")
print()
print(f"{"test":<<9}")
print(f"{"test":>>9}")
print(f"{"test":^^9}")
print()
print("Complex")

View File

@@ -77,6 +77,12 @@ class FormatSpecLexer:
def scan_token(self):
char: str = self.advance()
match char:
case "<":
self.add_token(TokenType.LEFT)
case ">":
self.add_token(TokenType.RIGHT)
case "^":
self.add_token(TokenType.CENTER)
case "+":
self.add_token(TokenType.PLUS)
case "-":
@@ -111,7 +117,7 @@ class FormatSpecLexer:
if char.isdigit():
self.scan_number()
else:
self.error("Unexpected character")
self.add_token(TokenType.CHAR)
return None
def scan_number(self):

View File

@@ -1,7 +1,7 @@
from typing import Optional
from src.core.format_spec.spec import FormatSpec, FormatSpecOptions, FormatSpecNumber, FormatSpecIntegral, \
FormatSpecDecimal
FormatSpecDecimal, FormatSpecAlignment
from src.core.format_spec.token import Token, TokenType
from src.parser.error import ParsingError
from src.pebble import Pebble
@@ -20,6 +20,18 @@ class FormatSpecParser:
TokenType.T_PCT,
}
ALIGNMENT: set[TokenType] = {
TokenType.LEFT,
TokenType.RIGHT,
TokenType.CENTER
}
SIGN: set[TokenType] = {
TokenType.PLUS,
TokenType.MINUS,
TokenType.SPACE
}
def __init__(self, tokens: list[Token]):
self.tokens: list[Token] = tokens
self.current: int = 0
@@ -68,22 +80,43 @@ class FormatSpecParser:
def spec(self) -> FormatSpec:
options: FormatSpecOptions = self.options()
number: FormatSpecNumber = self.number()
type: Optional[Token] = self.type()
fmt_type: Optional[Token] = self.type()
return FormatSpec(
options=options,
number=number,
type=type
type=fmt_type
)
def options(self) -> FormatSpecOptions:
alignment: Optional[FormatSpecAlignment] = self.alignment()
sign: Optional[Token] = None
if self.match(TokenType.PLUS, TokenType.MINUS, TokenType.SPACE):
if self.match(*self.SIGN):
sign = self.previous()
return FormatSpecOptions(
alignment=alignment,
sign=sign
)
def alignment(self) -> Optional[FormatSpecAlignment]:
if not self.match(TokenType.CHAR, *self.ALIGNMENT):
return None
fill: Optional[Token] = None
align: Token = self.previous()
if self.match(*self.ALIGNMENT):
fill = align
align = self.previous()
if align.type not in self.ALIGNMENT:
self.error(align, "Fill character without alignment.")
return None
return FormatSpecAlignment(
fill=fill,
align=align
)
def number(self) -> FormatSpecNumber:
integral: FormatSpecIntegral = self.integral()
decimal: Optional[FormatSpecDecimal] = self.decimal()

View File

@@ -4,8 +4,15 @@ from typing import Optional
from src.core.format_spec.token import Token
@dataclass(frozen=True)
class FormatSpecAlignment:
fill: Optional[Token]
align: Optional[Token]
@dataclass(frozen=True)
class FormatSpecOptions:
alignment: Optional[FormatSpecAlignment]
sign: Optional[Token]

View File

@@ -1,7 +1,7 @@
from math import log10, floor
from typing import Any, Optional
from src.core.format_spec.spec import FormatSpec
from src.core.format_spec.spec import FormatSpec, FormatSpecAlignment
from src.core.format_spec.token import TokenType, Token
from src.interpreter.error import PebbleRuntimeError
@@ -69,6 +69,8 @@ class StringFormatter:
else:
fmt_type, obj = self.guess_type(obj)
align_side: TokenType = TokenType.LEFT
fill_char: str = " "
match fmt_type:
case None:
res = self.stringify(obj)
@@ -76,15 +78,25 @@ class StringFormatter:
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)
align_side = TokenType.RIGHT
case TokenType.T_FIX:
res = self.format_float_fix(obj, spec, fmt_type)
align_side = TokenType.RIGHT
case TokenType.T_PCT:
res = self.format_float_fix(obj * 100, spec, TokenType.T_FIX) + "%"
align_side = TokenType.RIGHT
case TokenType.T_SCI:
res = self.format_float_sci(obj, spec, fmt_type)
align_side = TokenType.RIGHT
align_spec: Optional[FormatSpecAlignment] = spec.options.alignment
if align_spec is not None:
align_side = align_spec.align.type
if align_spec.fill is not None:
fill_char = align_spec.fill.lexeme
if spec.number.integral.width is not None:
res = self.pad(res, spec.number.integral.width)
res = self.pad(res, spec.number.integral.width, align_side, fill_char)
return res
def format_int(self, value: int, spec: FormatSpec, fmt_type: TokenType) -> str:
@@ -168,8 +180,21 @@ class StringFormatter:
groups.append(string[max(0, length - i - group_size):length - i])
return sep.join(reversed(groups))
def pad(self, string: str, width: int) -> str:
def pad(self, string: str, width: int, side: TokenType, char: str) -> str:
to_pad: int = width - len(string)
if to_pad > 0:
string = " " * to_pad + string
return string
if to_pad <= 0:
return string
left: int = 0
right: int = 0
match side:
case TokenType.LEFT:
left = 0
right = to_pad
case TokenType.RIGHT:
left = to_pad
right = 0
case TokenType.CENTER:
left = to_pad // 2
right = to_pad - left
return char * left + string + char * right

View File

@@ -6,6 +6,11 @@ from src.core.position import Position
class TokenType(Enum):
# Align
LEFT = auto()
RIGHT = auto()
CENTER = auto()
# Sign
PLUS = auto()
MINUS = auto()
@@ -35,6 +40,7 @@ class TokenType(Enum):
NUMBER = auto()
DOT = auto()
EOF = auto()
CHAR = auto()
@dataclass(frozen=True)