MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
DelimitedTextWriter.cpp
Go to the documentation of this file.
2
4
5#include <cctype>
6#include <charconv>
7#include <limits>
8
9namespace MayaFlux::IO {
10
11namespace {
12
13 std::string extension_of(const std::string& filepath)
14 {
15 auto extension = std::filesystem::path(filepath).extension().string();
16
17 std::ranges::transform(extension, extension.begin(),
18 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
19
20 return extension;
21 }
22
23 bool same_layout(
24 const Kakshya::CompositeLayout& left,
25 const Kakshya::CompositeLayout& right)
26 {
27 if (left.stride_bytes() != right.stride_bytes()
28 || left.presence_bytes() != right.presence_bytes()
29 || left.fields().size() != right.fields().size())
30 return false;
31
32 for (size_t i = 0; i < left.fields().size(); ++i) {
33 const auto& a = left.fields()[i];
34 const auto& b = right.fields()[i];
35
36 if (a.name != b.name || a.type != b.type
37 || a.offset_bytes != b.offset_bytes
38 || a.size_bytes != b.size_bytes)
39 return false;
40 }
41
42 return true;
43 }
44
45 template <typename T>
46 std::optional<std::string> format_number(
47 const Kakshya::Composite& element,
48 std::string_view name)
49 {
50 const auto value = element.get<T>(name);
51 if (!value)
52 return std::nullopt;
53
54 if constexpr (std::same_as<T, bool>) {
55 return *value ? "true" : "false";
56 } else {
57 std::array<char, 128> buffer {};
58
59 if constexpr (std::integral<T>) {
60 using Formatted = std::conditional_t<
61 std::is_signed_v<T>, long long, unsigned long long>;
62
63 const auto [end, error] = std::to_chars(
64 buffer.data(), buffer.data() + buffer.size(),
65 static_cast<Formatted>(*value));
66
67 if (error != std::errc {})
68 return std::nullopt;
69
70 return std::string(buffer.data(), end);
71 } else {
72 if (!std::isfinite(*value))
73 return std::nullopt;
74
75 const auto [end, error] = std::to_chars(
76 buffer.data(), buffer.data() + buffer.size(), *value);
77
78 if (error != std::errc {})
79 return std::nullopt;
80
81 return std::string(buffer.data(), end);
82 }
83 }
84 }
85
86 std::optional<std::string> format_field(
87 const Kakshya::Composite& element,
88 const Kakshya::CompositeField& field)
89 {
90 const auto name = std::string_view(field.name);
91 const auto type = field.type;
92
93 if (type == typeid(bool))
94 return format_number<bool>(element, name);
95
96 if (type == typeid(char))
97 return format_number<char>(element, name);
98 if (type == typeid(signed char))
99 return format_number<signed char>(element, name);
100 if (type == typeid(unsigned char))
101 return format_number<unsigned char>(element, name);
102
103 if (type == typeid(short))
104 return format_number<short>(element, name);
105 if (type == typeid(unsigned short))
106 return format_number<unsigned short>(element, name);
107
108 if (type == typeid(int))
109 return format_number<int>(element, name);
110 if (type == typeid(unsigned int))
111 return format_number<unsigned int>(element, name);
112
113 if (type == typeid(long))
114 return format_number<long>(element, name);
115 if (type == typeid(unsigned long))
116 return format_number<unsigned long>(element, name);
117
118 if (type == typeid(long long))
119 return format_number<long long>(element, name);
120 if (type == typeid(unsigned long long))
121 return format_number<unsigned long long>(element, name);
122
123 if (type == typeid(float))
124 return format_number<float>(element, name);
125 if (type == typeid(double))
126 return format_number<double>(element, name);
127 if (type == typeid(long double))
128 return format_number<long double>(element, name);
129
130 return std::nullopt;
131 }
132
133 bool supported_type(std::type_index type)
134 {
135 return type == typeid(std::string)
136 || type == typeid(bool)
137 || type == typeid(char)
138 || type == typeid(signed char)
139 || type == typeid(unsigned char)
140 || type == typeid(short)
141 || type == typeid(unsigned short)
142 || type == typeid(int)
143 || type == typeid(unsigned int)
144 || type == typeid(long)
145 || type == typeid(unsigned long)
146 || type == typeid(long long)
147 || type == typeid(unsigned long long)
148 || type == typeid(float)
149 || type == typeid(double)
150 || type == typeid(long double);
151 }
152
153}
154
156{
157 if (m_file.is_open())
158 close();
159}
160
161bool DelimitedTextWriter::set_write_header(bool enabled) noexcept
162{
163 if (is_open())
164 return false;
165
166 m_write_header = enabled;
167 return true;
168}
169
170bool DelimitedTextWriter::set_delimiter(char delimiter) noexcept
171{
172 if (is_open() || delimiter == '\0' || delimiter == '"'
173 || delimiter == '\r' || delimiter == '\n')
174 return false;
175
176 m_delimiter = delimiter;
177 return true;
178}
179
180bool DelimitedTextWriter::can_write(const std::string& filepath) const
181{
182 const auto extension = extension_of(filepath);
183
184 return extension == ".csv" || extension == ".tsv";
185}
186
188 const std::string& filepath,
189 const Kakshya::CompositeLayout& layout)
190{
191 if (is_open()) {
192 m_last_error = "Close the current file before opening another";
193 return false;
194 }
195
196 m_last_error.clear();
197
198 if (!can_write(filepath)) {
199 m_last_error = "Expected a .csv or .tsv output path";
200 return false;
201 }
202
203 if (layout.fields().empty()) {
204 m_last_error = "Composite output requires at least one field";
205 return false;
206 }
207
208 for (const auto& field : layout.fields()) {
209 if (!supported_type(field.type)) {
210 m_last_error = "Unsupported Composite field: " + field.name;
211 return false;
212 }
213 }
214
215 Kakshya::CompositeArray empty(layout);
216 m_layout = empty.layout();
219 : (extension_of(filepath) == ".tsv" ? '\t' : ',');
220
221 m_file.open(resolve_write_path(filepath),
222 std::ios::binary | std::ios::out | std::ios::trunc);
223
224 if (!m_file) {
225 m_last_error = "Cannot open delimited text output";
226 m_layout.reset();
227 return false;
228 }
229
230 m_rows_written = 0;
231
232 if (m_write_header) {
233 const auto& fields = m_layout->fields();
234
235 for (size_t i = 0; i < fields.size(); ++i) {
236 if (i != 0)
238
239 if (!write_cell(fields[i].name, false)) {
240 close();
241 return false;
242 }
243 }
244
245 m_file.put('\n');
246
247 if (!m_file) {
248 m_last_error = "Failed to write delimited text header";
249 close();
250 return false;
251 }
252 }
253
254 return true;
255}
256
259{
260 if (!is_open() || !m_layout) {
261 m_last_error = "No delimited text output is open";
262 return false;
263 }
264
265 if (!same_layout(*m_layout, rows.layout())) {
266 m_last_error = "Composite slice layout differs from the open file";
267 return false;
268 }
269
270 for (size_t i = 0; i < rows.size(); ++i) {
271 const auto element = rows.at(i);
272
273 if (!element || !write_element(*element))
274 return false;
275
277 }
278
279 return true;
280}
281
283{
284 if (!is_open()) {
285 m_last_error = "No delimited text output is open";
286 return false;
287 }
288
289 m_file.flush();
290
291 if (!m_file) {
292 m_last_error = "Failed to flush delimited text output";
293 return false;
294 }
295
296 return true;
297}
298
300{
301 if (!is_open())
302 return true;
303
304 m_file.flush();
305 m_file.close();
306
307 const bool finished = !m_file.fail();
308 if (!finished && m_last_error.empty())
309 m_last_error = "Failed to finalize delimited text output";
310
311 m_layout.reset();
312 return finished;
313}
314
316 std::string_view text, bool force_quote)
317{
318 const bool needs_quote = force_quote
319 || text.find(m_active_delimiter) != std::string_view::npos
320 || text.find('"') != std::string_view::npos
321 || text.find('\r') != std::string_view::npos
322 || text.find('\n') != std::string_view::npos;
323
324 if (needs_quote)
325 m_file.put('"');
326
327 if (needs_quote) {
328 for (const char c : text) {
329 if (c == '"')
330 m_file.put('"');
331
332 m_file.put(c);
333 }
334 } else {
335 if (text.size() > static_cast<size_t>(
336 std::numeric_limits<std::streamsize>::max())) {
337 m_last_error = "Delimited text cell exceeds stream size";
338 return false;
339 }
340
341 m_file.write(text.data(), static_cast<std::streamsize>(text.size()));
342 }
343
344 if (needs_quote)
345 m_file.put('"');
346
347 if (!m_file) {
348 m_last_error = "Failed to write delimited text cell";
349 return false;
350 }
351
352 return true;
353}
354
356 const Kakshya::Composite& element)
357{
358 const auto& fields = m_layout->fields();
359
360 for (size_t i = 0; i < fields.size(); ++i) {
361 if (i != 0)
363
364 const auto& field = fields[i];
365 if (!element.has(field.name))
366 continue;
367
368 if (field.type == typeid(std::string)) {
369 const auto text = element.text(field.name);
370
371 if (!text || !write_cell(*text, text->empty())) {
372 if (m_last_error.empty())
373 m_last_error = "Invalid text field: " + field.name;
374 return false;
375 }
376 } else {
377 const auto text = format_field(element, field);
378
379 if (!text || !write_cell(*text, false)) {
380 if (m_last_error.empty())
381 m_last_error = "Invalid numeric field: " + field.name;
382 return false;
383 }
384 }
385 }
386
387 m_file.put('\n');
388
389 if (!m_file) {
390 m_last_error = "Failed to write delimited text element";
391 return false;
392 }
393
394 return true;
395}
396
397}
bool size
uint32_t rows
std::shared_ptr< NetworkGeometryBuffer > buffer
size_t a
size_t b
glm::vec3 value
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
std::string name
Definition VKDevice.cpp:123
bool set_delimiter(char delimiter) noexcept
Override the extension-selected delimiter.
bool write_cell(std::string_view text, bool force_quote)
bool write_rows(const Kakshya::CompositeSlice &rows) override
Encode a bounded slice using the open file's layout.
bool flush() override
Flush the current file without closing it.
bool set_write_header(bool enabled) noexcept
Select whether a header record is written.
bool write_element(const Kakshya::Composite &element)
bool close() override
Finalize and close the current file.
bool is_open() const override
Whether an output file is open.
bool can_write(const std::string &filepath) const override
Check for a .csv or .tsv output path.
std::optional< Kakshya::CompositeLayout > m_layout
bool open(const std::string &filepath, const Kakshya::CompositeLayout &layout) override
Create or truncate a CSV or TSV file.
const CompositeLayout & layout() const noexcept
Finalized layout shared by all elements.
Owns a sequence of values that share a CompositeLayout.
const std::vector< CompositeField > & fields() const noexcept
Declared fields in element order, with finalized byte offsets.
Definition Composite.hpp:82
Shared field description and byte layout for a CompositeArray.
Definition Composite.hpp:52
Borrowed consecutive elements, optionally selected by a Region.
std::optional< std::string_view > text(std::string_view field_name) const noexcept
Read a present std::string field as borrowed UTF-8 bytes.
bool has(std::string_view field_name) const noexcept
Whether a named field exists and is present in this element.
Read view of one value in a CompositeArray.
std::string resolve_write_path(const std::string &filepath, std::string_view default_directory=Config::OUT_DIR)
Resolve a writable path against the configured output directory.
void error(Component component, Context context, std::source_location location, std::string_view message)
Log an error message and optionally throw an exception.