Skip to content

Instantly share code, notes, and snippets.

@tibicen
Created June 18, 2025 17:26
Show Gist options
  • Save tibicen/f349330c231e5cf0ab0a6cd7d9d375c8 to your computer and use it in GitHub Desktop.
Save tibicen/f349330c231e5cf0ab0a6cd7d9d375c8 to your computer and use it in GitHub Desktop.
Typst IFC documents
// PRICED BILL OF QUANTITIES TEMPLATE
// author: carlo pavan
// year: 2025
#let euro(num) = {
str(calc.round(float(num), digits: 2)) + " €"
}
#let unit_map = (
"METRE": "m",
"SQUARE_METRE": "m²",
"m2": "m²",
"CUBIC_METRE": "m³",
"m3": "m³",
"VOLUMEUNIT / CUBIC_METRE": "m³",
"KILOGRAM": "kg",
// add more mappings as needed
)
#let format-decimal(num, places: 2) = {
let rounded = calc.round(num, digits: places)
let str-num = str(rounded)
// Split into integer and decimal parts
let parts = str-num.split(".")
let integer-part = parts.at(0)
let decimal-part = parts.at(1, default: "")
// Add thousand separators to integer part
let formatted-integer = ""
let chars = integer-part.clusters().rev()
for (i, char) in chars.enumerate() {
if i > 0 and calc.rem(i, 3) == 0 {
formatted-integer = "'" + formatted-integer
}
formatted-integer = char + formatted-integer
}
// Ensure decimal part has correct number of places
decimal-part = decimal-part + "0" * (places - decimal-part.len())
formatted-integer + "." + decimal-part
}
#let read-csv(path, delimiter: ",") = {
let lines = read(path).split("\n").filter(l => l != "")
let header = lines.at(0).split(delimiter).map(f => f.trim())
let rows = lines.slice(1).map(line => {
let values = line.split(delimiter).map(f => f.trim())
let row-dict = (:)
for (i, col) in header.enumerate() {
row-dict.insert(col, values.at(i, default: ""))
}
row-dict
})
(header: header, rows: rows)
}
#let arrange_row(row) = {
let description = [#text(8pt,weight: "bold", row.at(4)) \ #text(8pt, row.at(3)) \ #text(8pt, lorem(25)) \ #align(right)[Sum]]
let quant = if row.at(5) == "" {0.0} else {float(row.at(5))}
let rate = if row.at(7) == "" {0.0} else {float(row.at(7))}
let total = if row.at(8) == "" {0.0} else {float(row.at(8))}
(
row.at(1),
description,
[],
[],
[],
[],
align(right + bottom)[#format-decimal(quant)],
align(right + bottom)[#format-decimal(rate)],
align(right + bottom)[#format-decimal(total)],
)
}
#let csv-table-schedule(path, delimiter: ",") = {
// let data = read-csv(path, delimiter: delimiter)
// let new_rows = ()
let data = csv(path, delimiter: delimiter)
let header = data.remove(0)
let new_rows = data.map(arrange_row) // TODO: Do transformation on data to fit your needs
table(
columns: (auto,1fr, 12mm,12mm,12mm,12mm, auto, auto, auto),
align: (left, left, center, center, center, center, right, right, right),
stroke: (x, y) => (
left: if x == 0 { 1pt } else { 0.25pt },
right: 1pt,
top: if y < 2 {1pt} else {0pt},
bottom: 1pt
),
table.header([Hierarchy], [Description], [n°],[l],[w],[h/w], [Quantity], [Rate], [Total]),
..new_rows.flatten()
)
}
#let csv-table-summary(path, delimiter: ",") = {
let data = read-csv(path, delimiter: delimiter)
let new_rows = ()
for row in data.rows {
let new-cell = ()
if row.at("TotalPrice") != "0.0" {
new-cell.push(row.at("Hierarchy"))
new-cell.push(row.at("Name"))
new-cell.push(row.at("General Cost"))
new-cell.push(format-decimal(float(row.at("TotalPrice")), places: 2))
}
new_rows.push(new-cell)
}
set text(size: 10pt)
pad(left: 2cm)[SUMMARY:]
set text(size: 8pt)
table(
columns: (18mm,107mm, 30mm, 30mm),
align: (center, left, right, right),
stroke: (x, y) => (
left: none,
right: none,
top: (dash: "dotted"),
bottom: (dash: "dotted")
),
..new_rows.flatten()
)
}
#let project(
title: "",
schedule_name: "",
cover_page: bool,
root_items_to_new_page: bool,
summary: bool,
body) = {
// Set the document's basic properties.
//set document(schedule: schedule_name, title: title)
set page(
margin: (left: 15mm, right: 10mm, top: 35mm, bottom: 20mm),
numbering: "1/1",
number-align: end,
header:[
#set text(font: "Liberation Sans", size: 9pt, lang: "en");
#title
#h(1fr)
#schedule_name
],
footer: context [
#datetime.today().display("[day]/[month]/[year]")
#h(1fr)
#counter(page).display("1/1", both: true)
],
)
set text(font: "Liberation Sans", size: 8pt, lang: "en");
csv-table-schedule("schedule.csv")
if summary == true {
pagebreak()
set text(font: "Liberation Sans", size: 8pt, lang: "en");
set page(
background:
place( top + left, dx: 15mm, dy: 25mm,
table(columns: (18mm,107mm, 30mm, 30mm),
rows: (6mm, 245mm),
align: (center, left, center, center, center, center, center, center, center),
stroke: (x, y) => (
left: if x == 0 { 1pt } else { 0.25pt },
right: 1pt,
top: 1pt,
bottom: 1pt
),
text(size: 8pt)[Hierarchy],
text(size: 8pt)[Description],
text(size: 8pt)[Sub Total],
text(size: 8pt)[Total]
)
)
)
set text(font: "Liberation Sans", size: 8pt, lang: "en");
csv-table-summary("schedule.csv")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment