feat(fstring): add alignment
This commit is contained in:
@@ -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")
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user