feat(fstring): add alignment
This commit is contained in:
@@ -45,4 +45,15 @@ print(f"{d2:e}")
|
|||||||
print(f"{0:e}")
|
print(f"{0:e}")
|
||||||
print()
|
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")
|
print("Complex")
|
||||||
@@ -77,6 +77,12 @@ class FormatSpecLexer:
|
|||||||
def scan_token(self):
|
def scan_token(self):
|
||||||
char: str = self.advance()
|
char: str = self.advance()
|
||||||
match char:
|
match char:
|
||||||
|
case "<":
|
||||||
|
self.add_token(TokenType.LEFT)
|
||||||
|
case ">":
|
||||||
|
self.add_token(TokenType.RIGHT)
|
||||||
|
case "^":
|
||||||
|
self.add_token(TokenType.CENTER)
|
||||||
case "+":
|
case "+":
|
||||||
self.add_token(TokenType.PLUS)
|
self.add_token(TokenType.PLUS)
|
||||||
case "-":
|
case "-":
|
||||||
@@ -111,7 +117,7 @@ class FormatSpecLexer:
|
|||||||
if char.isdigit():
|
if char.isdigit():
|
||||||
self.scan_number()
|
self.scan_number()
|
||||||
else:
|
else:
|
||||||
self.error("Unexpected character")
|
self.add_token(TokenType.CHAR)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def scan_number(self):
|
def scan_number(self):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.core.format_spec.spec import FormatSpec, FormatSpecOptions, FormatSpecNumber, FormatSpecIntegral, \
|
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.core.format_spec.token import Token, TokenType
|
||||||
from src.parser.error import ParsingError
|
from src.parser.error import ParsingError
|
||||||
from src.pebble import Pebble
|
from src.pebble import Pebble
|
||||||
@@ -20,6 +20,18 @@ class FormatSpecParser:
|
|||||||
TokenType.T_PCT,
|
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]):
|
def __init__(self, tokens: list[Token]):
|
||||||
self.tokens: list[Token] = tokens
|
self.tokens: list[Token] = tokens
|
||||||
self.current: int = 0
|
self.current: int = 0
|
||||||
@@ -68,22 +80,43 @@ class FormatSpecParser:
|
|||||||
def spec(self) -> FormatSpec:
|
def spec(self) -> FormatSpec:
|
||||||
options: FormatSpecOptions = self.options()
|
options: FormatSpecOptions = self.options()
|
||||||
number: FormatSpecNumber = self.number()
|
number: FormatSpecNumber = self.number()
|
||||||
type: Optional[Token] = self.type()
|
fmt_type: Optional[Token] = self.type()
|
||||||
return FormatSpec(
|
return FormatSpec(
|
||||||
options=options,
|
options=options,
|
||||||
number=number,
|
number=number,
|
||||||
type=type
|
type=fmt_type
|
||||||
)
|
)
|
||||||
|
|
||||||
def options(self) -> FormatSpecOptions:
|
def options(self) -> FormatSpecOptions:
|
||||||
|
alignment: Optional[FormatSpecAlignment] = self.alignment()
|
||||||
sign: Optional[Token] = None
|
sign: Optional[Token] = None
|
||||||
if self.match(TokenType.PLUS, TokenType.MINUS, TokenType.SPACE):
|
if self.match(*self.SIGN):
|
||||||
sign = self.previous()
|
sign = self.previous()
|
||||||
|
|
||||||
return FormatSpecOptions(
|
return FormatSpecOptions(
|
||||||
|
alignment=alignment,
|
||||||
sign=sign
|
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:
|
def number(self) -> FormatSpecNumber:
|
||||||
integral: FormatSpecIntegral = self.integral()
|
integral: FormatSpecIntegral = self.integral()
|
||||||
decimal: Optional[FormatSpecDecimal] = self.decimal()
|
decimal: Optional[FormatSpecDecimal] = self.decimal()
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ from typing import Optional
|
|||||||
from src.core.format_spec.token import Token
|
from src.core.format_spec.token import Token
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormatSpecAlignment:
|
||||||
|
fill: Optional[Token]
|
||||||
|
align: Optional[Token]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class FormatSpecOptions:
|
class FormatSpecOptions:
|
||||||
|
alignment: Optional[FormatSpecAlignment]
|
||||||
sign: Optional[Token]
|
sign: Optional[Token]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from math import log10, floor
|
from math import log10, floor
|
||||||
from typing import Any, Optional
|
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.core.format_spec.token import TokenType, Token
|
||||||
from src.interpreter.error import PebbleRuntimeError
|
from src.interpreter.error import PebbleRuntimeError
|
||||||
|
|
||||||
@@ -69,6 +69,8 @@ class StringFormatter:
|
|||||||
else:
|
else:
|
||||||
fmt_type, obj = self.guess_type(obj)
|
fmt_type, obj = self.guess_type(obj)
|
||||||
|
|
||||||
|
align_side: TokenType = TokenType.LEFT
|
||||||
|
fill_char: str = " "
|
||||||
match fmt_type:
|
match fmt_type:
|
||||||
case None:
|
case None:
|
||||||
res = self.stringify(obj)
|
res = self.stringify(obj)
|
||||||
@@ -76,15 +78,25 @@ class StringFormatter:
|
|||||||
res = obj
|
res = obj
|
||||||
case TokenType.T_BIN | TokenType.T_DEC | TokenType.T_HEX | TokenType.T_HEX_CAPS | TokenType.T_OCT:
|
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)
|
res = self.format_int(obj, spec, fmt_type)
|
||||||
|
align_side = TokenType.RIGHT
|
||||||
case TokenType.T_FIX:
|
case TokenType.T_FIX:
|
||||||
res = self.format_float_fix(obj, spec, fmt_type)
|
res = self.format_float_fix(obj, spec, fmt_type)
|
||||||
|
align_side = TokenType.RIGHT
|
||||||
case TokenType.T_PCT:
|
case TokenType.T_PCT:
|
||||||
res = self.format_float_fix(obj * 100, spec, TokenType.T_FIX) + "%"
|
res = self.format_float_fix(obj * 100, spec, TokenType.T_FIX) + "%"
|
||||||
|
align_side = TokenType.RIGHT
|
||||||
case TokenType.T_SCI:
|
case TokenType.T_SCI:
|
||||||
res = self.format_float_sci(obj, spec, fmt_type)
|
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:
|
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
|
return res
|
||||||
|
|
||||||
def format_int(self, value: int, spec: FormatSpec, fmt_type: TokenType) -> str:
|
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])
|
groups.append(string[max(0, length - i - group_size):length - i])
|
||||||
return sep.join(reversed(groups))
|
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)
|
to_pad: int = width - len(string)
|
||||||
if to_pad > 0:
|
if to_pad <= 0:
|
||||||
string = " " * to_pad + string
|
return string
|
||||||
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):
|
class TokenType(Enum):
|
||||||
|
# Align
|
||||||
|
LEFT = auto()
|
||||||
|
RIGHT = auto()
|
||||||
|
CENTER = auto()
|
||||||
|
|
||||||
# Sign
|
# Sign
|
||||||
PLUS = auto()
|
PLUS = auto()
|
||||||
MINUS = auto()
|
MINUS = auto()
|
||||||
@@ -35,6 +40,7 @@ class TokenType(Enum):
|
|||||||
NUMBER = auto()
|
NUMBER = auto()
|
||||||
DOT = auto()
|
DOT = auto()
|
||||||
EOF = auto()
|
EOF = auto()
|
||||||
|
CHAR = auto()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user