feat: Initial csv parsing for uploading rooms for a project. Need to style the upload form.
All checks were successful
CI / Linux Tests (push) Successful in 5m41s

This commit is contained in:
2026-02-05 16:39:40 -05:00
parent b2b5e32535
commit 57766c990e
16 changed files with 226 additions and 117 deletions

View File

@@ -0,0 +1,22 @@
import CSVParser
import Foundation
import Testing
@Suite
struct CSVParsingTests {
@Test
func roomParsing() async throws {
let parser = CSVParser.liveValue
let input = """
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count
Bed-1,12345,12345,,2
Bed-2,1223,,1123,1
"""
let rooms = try await parser.parseRooms(.init(file: Data(input.utf8)))
#expect(rooms.count == 2)
}
}

View File

@@ -85,7 +85,8 @@ struct ProjectTests {
#expect(completed.frictionRate == true)
_ = try await database.rooms.create(
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345)
project.id,
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
)
completed = try await database.projects.getCompletedSteps(project.id)
#expect(completed.rooms == true)
@@ -130,7 +131,8 @@ struct ProjectTests {
.init(projectID: project.id, name: "Test", value: 0.2)
)
let room = try await database.rooms.create(
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345)
project.id,
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
)
let supplyLength = try await database.equivalentLengths.create(
.init(

View File

@@ -15,7 +15,8 @@ struct RoomTests {
@Dependency(\.database.rooms) var rooms
let room = try await rooms.create(
.init(projectID: project.id, name: "Test", heatingLoad: 1234, coolingTotal: 1234)
project.id,
.init(name: "Test", heatingLoad: 1234, coolingTotal: 1234)
)
let fetched = try await rooms.fetch(project.id)
@@ -48,10 +49,13 @@ struct RoomTests {
try await withTestUserAndProject { _, project in
@Dependency(\.database.rooms) var rooms
let created = try await rooms.createMany([
.init(projectID: project.id, name: "Test 1", heatingLoad: 1234, coolingTotal: 1234),
.init(projectID: project.id, name: "Test 2", heatingLoad: 1234, coolingTotal: 1234),
])
let created = try await rooms.createMany(
project.id,
[
.init(name: "Test 1", heatingLoad: 1234, coolingTotal: 1234),
.init(name: "Test 2", heatingLoad: 1234, coolingTotal: 1234),
]
)
#expect(created.count == 2)
#expect(created[0].name == "Test 1")
@@ -85,7 +89,7 @@ struct RoomTests {
@Test(
arguments: [
Room.Create(
projectID: UUID(0),
// projectID: UUID(0),
name: "",
heatingLoad: 12345,
coolingTotal: 12344,
@@ -93,7 +97,7 @@ struct RoomTests {
registerCount: 1
),
Room.Create(
projectID: UUID(0),
// projectID: UUID(0),
name: "Test",
heatingLoad: -12345,
coolingTotal: 12344,
@@ -101,7 +105,7 @@ struct RoomTests {
registerCount: 1
),
Room.Create(
projectID: UUID(0),
// projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: -12344,
@@ -109,7 +113,7 @@ struct RoomTests {
registerCount: 1
),
Room.Create(
projectID: UUID(0),
// projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: 12344,
@@ -117,7 +121,7 @@ struct RoomTests {
registerCount: 1
),
Room.Create(
projectID: UUID(0),
// projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: 12344,
@@ -125,7 +129,7 @@ struct RoomTests {
registerCount: -1
),
Room.Create(
projectID: UUID(0),
// projectID: UUID(0),
name: "",
heatingLoad: -12345,
coolingTotal: -12344,
@@ -133,7 +137,7 @@ struct RoomTests {
registerCount: -1
),
Room.Create(
projectID: UUID(0),
// projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: nil,
@@ -145,89 +149,12 @@ struct RoomTests {
func validations(room: Room.Create) throws {
#expect(throws: (any Error).self) {
// do {
try room.toModel().validate()
try room.toModel(projectID: UUID(0)).validate()
// } catch {
// print("\(error)")
// throw error
// }
}
}
@Test
func csvParsing() throws {
let input = """
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count
Bed-1,12345,12345,,2
Bed-2,1223,,1123,1
"""[...].utf8
let commaSeparator = ParsePrint {
OneOf {
",".utf8
", ".utf8
}
}
let rowParser = ParsePrint {
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
",".utf8
Double.parser()
Skip { commaSeparator }
// ",".utf8
Optionally {
Double.parser()
}
Skip { commaSeparator }
// ",".utf8
Optionally {
Double.parser()
}
Skip { commaSeparator }
// ",".utf8
Int.parser()
}
.map(.memberwise(Row.init))
let csvRowParser = OneOf {
rowParser.map { CSVRowType.row($0) }
Prefix { $0 != UInt8(ascii: "\n") }.map(.string).map { CSVRowType.header($0) }
}
let rowsParser = Many {
csvRowParser
} separator: {
"\n".utf8
}
let rows = try rowsParser.parse(input)
print("rows: \(rows)")
#expect(rows.count == 3)
#expect(rows.rows.count == 2)
print(String(try rowParser.print(rows.rows.first!))!)
}
}
enum CSVRowType {
case header(String)
case row(Row)
}
struct Row {
let name: String
let heatingLoad: Double
let coolingTotal: Double?
let coolingSensible: Double?
let registerCount: Int
}
extension Array where Element == CSVRowType {
var rows: [Row] {
reduce(into: [Row]()) {
if case .row(let row) = $1 {
$0.append(row)
}
}
}
}

View File

@@ -14,9 +14,11 @@ struct TrunkSizeTests {
@Dependency(\.database) var database
let room = try await database.rooms.create(
project.id,
.init(
projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345,
coolingSensible: nil, registerCount: 5)
name: "Test", heatingLoad: 12345, coolingTotal: 12345,
coolingSensible: nil, registerCount: 5
)
)
let trunk = try await database.trunkSizes.create(

View File

@@ -63,6 +63,12 @@ p-6 w-full">
</div>
</button>
</div>
<div class="tooltip tooltip-left" data-tip="Upload csv file">
<form hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms/csv" hx-target="body" hx-swap="outerHTML" enctype="multipart/form-data">
<input type="file" name="file" accept=".csv">
<button class="btn btn-secondary" type="submit">Submit</button>
</form>
</div>
</div>
<div class="flex items-end space-x-4 font-bold">
<span class="text-lg">Heating Total</span>
@@ -151,7 +157,6 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000001.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000001" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001">
Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Bed-1">
@@ -204,7 +209,6 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000002.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000002" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002">
Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Entry">
@@ -257,7 +261,6 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000003.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000003" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003">
Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Family Room">
@@ -310,7 +313,6 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000004.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000004" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004">
Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Kitchen">
@@ -363,7 +365,6 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000005.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000005" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005">
Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Living Room">
@@ -416,7 +417,6 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000006.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000006" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006">
Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Master">
@@ -441,8 +441,7 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
Name<label class="input w-full"><span class="label"></span>
<label class="input w-full"><span class="label">Name</span>
<input name="name" type="text" placeholder="Name" required autofocus value="">
Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="">