Comparison
Side-by-side view of full Python and Raku Invoice DSL parser code
Open this Raku in the playground.
"""Python Lark-grammar parser for the invoice DSL. (130 loc)"""
from dataclasses import dataclass, field
from lark import Lark, Transformer
GRAMMAR = r"""
start: invoice_line (_NL | field_line | item_line | tax_line)*
invoice_line: _WS? "invoice" _WS ID _NL
field_line: _WS? "date" _WS DATE _NL
| _WS? "client" _WS ESCAPED_STRING _NL
item_line: _WS? "item" _WS ESCAPED_STRING _WS "hours" \
_WS NUMBER _WS "rate" _WS NUMBER _NL
tax_line: _WS? "tax" _WS PERCENT _NL
DATE: /\d{4}-\d{2}-\d{2}/
ID: /[A-Za-z0-9_-]+/
PERCENT: /\d+(\.\d+)?%/
_NL: /[ \t]*\r?\n/
_WS: /[ \t]+/
%import common.NUMBER
%import common.ESCAPED_STRING
"""
@dataclass
class Item:
description: str
hours: float
rate: float
@property
def subtotal(self):
return self.hours * self.rate
@dataclass
class Invoice:
id: str = ""
date: str = ""
client: str = ""
items: list = field(default_factory=list)
tax_rate: float = 0.0
@property
def subtotal(self):
return sum(i.subtotal for i in self.items)
@property
def tax(self):
return self.subtotal * self.tax_rate
@property
def total(self):
return self.subtotal + self.tax
class InvoiceTransformer(Transformer):
def start(self, items):
inv = Invoice()
for item in items:
if isinstance(item, dict):
inv.__dict__.update(item)
elif isinstance(item, Item):
inv.items.append(item)
return inv
def invoice_line(self, tokens):
return {"id": str(tokens[0])}
def field_line(self, tokens):
# tokens[0] is either DATE or ESCAPED_STRING; infer key from type
token = tokens[0]
if token.type == "DATE":
return {"date": str(token)}
else:
return {"client": str(token).strip('"')}
def item_line(self, tokens):
desc = str(tokens[0]).strip('"')
hours = float(tokens[1])
rate = float(tokens[2])
return Item(desc, hours, rate)
def tax_line(self, tokens):
return {"tax_rate": float(str(tokens[0]).rstrip("%")) / 100}
_parser = Lark(GRAMMAR, parser="earley", ambiguity="resolve")
def parse(text: str) -> Invoice:
tree = _parser.parse(text.strip() + "\n")
return InvoiceTransformer().transform(tree)
def render(inv: Invoice) -> str:
lines = [
f"Invoice: {inv.id}",
f"Date: {inv.date}",
f"Client: {inv.client}",
"",
f"{'Description':<30} {'Hours':>6} {'Rate':>8} {'Subtotal':>10}",
"-" * 58,
]
for item in inv.items:
lines.append(f"{item.description:<30} {item.hours:>6.1f} \
{item.rate:>8.2f} {item.subtotal:>10.2f}")
lines += [
"-" * 58,
f"{'Subtotal':>46} {inv.subtotal:>10.2f}",
f"{'Tax (' + str(int(inv.tax_rate * 100)) + '%)':>46} {inv.tax:>10.2f}",
f"{'Total':>46} {inv.total:>10.2f}",
]
return "\n".join(lines)
EXAMPLE = """\
invoice INV-001
date 2026-04-29
client "Acme Corp"
item "Website redesign" hours 10 rate 150
item "Hosting setup" hours 2 rate 100
tax 8%
"""
if __name__ == "__main__":
inv = parse(EXAMPLE)
print(render(inv))
#| Raku Grammar parser for the Invoice DSL. (80 loc)
grammar Grammar {
token TOP {
<invoice-line>
[ \n+ <.ws> [ <field-line> | <item-line> ] ]*
\n*
}
rule invoice-line { invoice <id> }
rule field-line { | date <date>
| client <client=quoted>
| tax <tax-rate=number> '%' }
rule item-line { item <description=quoted>
hours <hours=number>
rate <rate=number> }
token ws { \h* } #horizontal whitespace only
token id { <[A..Za..z0..9_-]>+ }
token date { \d**4 '-' \d**2 '-' \d**2 }
token number { \d+ [ '.' \d+ ]? }
token quoted { '"' <( <-["]>+ )> '"' }
}
class Item {
has ($.description, $.hours, $.rate);
method subtotal { $!hours * $!rate }
}
class Invoice {
has ($.id, $.date, $.client);
has $.tax-rate = 0;
has Item @.items;
method subtotal { @!items.map(*.subtotal).sum }
method tax { $.subtotal * $!tax-rate }
method total { $.subtotal + $.tax }
method label { "Tax ({($!tax-rate * 100).Int}%)" }
method raku { render(self) }
}
class Actions {
method info-line($/) {
make $<date> ?? { date => ~$<date> }
!! $<client> ?? { client => ~$<client> }
!! { tax-rate => (+$<tax-rate>) / 100 }
}
method item-line($/) {
make Item.new(description => ~$<description>,
hours => +$<hours>,
rate => +$<rate>)
}
method TOP($/) {
make Invoice.new( id => ~$<head-line><id>,
|%($<info-line>.map(*.made)),
items => $<item-line>.map(*.made).Array);
}
}
sub render($inv) {
given $inv {qq:to/RENDER/;
Invoice: .id
Date: .date
Client: .client
{ sprintf("%-30s %6s %8s %10s",
"Description", "Hours", "Rate", "Subtotal") }
{ "-" x 58 }
{ .items.map({ sprintf("%-30s %6.1f %8.2f %10.2f",
.description, .hours, .rate, .subtotal) }).join("\n") }
{ "-" x 58 }
{ sprintf("%46s %10.2f", "Subtotal", .subtotal) }
{ sprintf("%46s %10.2f", .label, .tax) }
{ sprintf("%46s %10.2f", "Total", .total) }
RENDER
}
}
my $EXAMPLE = q:to/END/;
invoice INV-001
date 2026-04-29
client "Acme Corp"
item "Website redesign" hours 10 rate 150
item "Hosting setup" hours 2 rate 100
tax 8%
END
say Grammar.parse($text, :actions(Actions.new)).made;