open-wether / Sources /App /Helper /Writer /CsvWriter.swift
soiz1's picture
Migrated from GitHub
6ee917b verified
import Foundation
import Vapor
extension ForecastapiResult {
/// Streaming CSV format. Once 3kb of text is accumulated, flush to next handler -> response compressor
func toCsvResponse() throws -> Response {
let response = Response(body: .init(stream: { writer in
_ = writer.eventLoop.makeFutureWithTask {
var b = BufferAndWriter(writer: writer)
let multiLocation = results.count > 1
if results.count == 1, let location = results.first, let first = location.results.first {
b.buffer.writeString("latitude,longitude,elevation,utc_offset_seconds,timezone,timezone_abbreviation\n")
let elevation = first.elevation.map({ $0.isFinite ? "\($0)" : "NaN" }) ?? "NaN"
b.buffer.writeString("\(first.latitude),\(first.longitude),\(elevation),\(location.utc_offset_seconds),\(location.timezone.identifier),\(location.timezone.abbreviation)\n")
} else {
b.buffer.writeString("location_id,latitude,longitude,elevation,utc_offset_seconds,timezone,timezone_abbreviation\n")
for location in results {
guard let first = location.results.first else {
continue
}
let elevation = first.elevation.map({ $0.isFinite ? "\($0)" : "NaN" }) ?? "NaN"
b.buffer.writeString("\(location.locationId),\(first.latitude),\(first.longitude),\(elevation),\(location.utc_offset_seconds),\(location.timezone.identifier),\(location.timezone.abbreviation)\n")
}
}
for location in results {
try await location.current?().writeCsv(into: &b, timeformat: timeformat, utc_offset_seconds: location.utc_offset_seconds, location_id: multiLocation ? location.locationId : nil)
}
for location in results {
try await location.minutely15?().writeCsv(into: &b, timeformat: timeformat, utc_offset_seconds: location.utc_offset_seconds, location_id: multiLocation ? location.locationId : nil)
}
for location in results {
try await location.hourly?().writeCsv(into: &b, timeformat: timeformat, utc_offset_seconds: location.utc_offset_seconds, location_id: multiLocation ? location.locationId : nil)
}
for location in results {
try await location.sixHourly?().writeCsv(into: &b, timeformat: timeformat, utc_offset_seconds: location.utc_offset_seconds, location_id: multiLocation ? location.locationId : nil)
}
for location in results {
try await location.daily?().writeCsv(into: &b, timeformat: timeformat, utc_offset_seconds: location.utc_offset_seconds, location_id: multiLocation ? location.locationId : nil)
}
try await b.flush()
try await b.end()
}
}, count: -1))
response.headers.replaceOrAdd(name: .contentType, value: "text/csv; charset=utf-8")
response.headers.replaceOrAdd(name: .contentDisposition, value: "attachment; filename=\"open-meteo-\(results.first?.results.first?.formatedCoordinatesFilename ?? "").csv\"")
return response
}
}
extension ApiSectionSingle {
fileprivate func writeCsv(into b: inout BufferAndWriter, timeformat: Timeformat, utc_offset_seconds: Int, location_id: Int?) async throws {
if location_id == nil || location_id == 0 {
b.buffer.writeString("\n")
if location_id != nil {
b.buffer.writeString("location_id,time")
} else {
b.buffer.writeString("time")
}
for e in columns {
b.buffer.writeString(",\(e.variable) (\(e.unit.abbreviation))")
}
b.buffer.writeString("\n")
}
let time = self.time.formated(format: timeformat, utc_offset_seconds: utc_offset_seconds, quotedString: false)
if let location_id {
b.buffer.writeString("\(location_id),")
}
b.buffer.writeString(time)
for e in columns {
if e.value.isFinite {
b.buffer.writeString(",\(String(format: "%.\(e.unit.significantDigits)f", e.value))")
} else {
b.buffer.writeString(",NaN")
}
}
b.buffer.writeString("\n")
try await b.flushIfRequired()
}
}
extension ApiSectionString {
/// Write a single API section into the output buffer
fileprivate func writeCsv(into b: inout BufferAndWriter, timeformat: Timeformat, utc_offset_seconds: Int, location_id: Int?) async throws {
if location_id == nil || location_id == 0 {
b.buffer.writeString("\n")
if location_id != nil {
b.buffer.writeString("location_id,time")
} else {
b.buffer.writeString("time")
}
for e in columns {
b.buffer.writeString(",\(e.variable) (\(e.unit.abbreviation))")
}
b.buffer.writeString("\n")
}
for (i, time) in time.itterate(format: timeformat, utc_offset_seconds: utc_offset_seconds, quotedString: false, onlyDate: time.dtSeconds == 86400).enumerated() {
if let location_id {
b.buffer.writeString("\(location_id),")
}
b.buffer.writeString(time)
for e in columns {
switch e.data {
case .float(let a):
if a[i].isFinite {
b.buffer.writeString(",\(String(format: "%.\(e.unit.significantDigits)f", a[i]))")
} else {
b.buffer.writeString(",NaN")
}
case .timestamp(let a):
switch timeformat {
case .iso8601:
b.buffer.writeString(",\(a[i].add(utc_offset_seconds).iso8601_YYYY_MM_dd_HH_mm)")
case .unixtime:
b.buffer.writeString(",\(a[i].timeIntervalSince1970)")
}
}
}
b.buffer.writeString("\n")
try await b.flushIfRequired()
}
}
}