Line data Source code
1 : /****************************************************************************/
2 : // Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3 : // Copyright (C) 2012-2026 German Aerospace Center (DLR) and others.
4 : // This program and the accompanying materials are made available under the
5 : // terms of the Eclipse Public License 2.0 which is available at
6 : // https://www.eclipse.org/legal/epl-2.0/
7 : // This Source Code may also be made available under the following Secondary
8 : // Licenses when the conditions for such availability set forth in the Eclipse
9 : // Public License 2.0 are satisfied: GNU General Public License, version 2
10 : // or later which is available at
11 : // https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12 : // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13 : /****************************************************************************/
14 : /// @file CSVFormatter.h
15 : /// @author Michael Behrisch
16 : /// @date 2025-06-12
17 : ///
18 : // Output formatter for CSV output
19 : /****************************************************************************/
20 : #pragma once
21 : #include <config.h>
22 :
23 : #include <memory>
24 : #include <algorithm>
25 : #include "OutputFormatter.h"
26 : #include <utils/common/ToString.h>
27 : #include <utils/common/Translation.h>
28 :
29 :
30 : // ===========================================================================
31 : // class definitions
32 : // ===========================================================================
33 : /**
34 : * @class CSVFormatter
35 : * @brief Output formatter for CSV output
36 : */
37 : class CSVFormatter : public OutputFormatter {
38 : public:
39 : /// @brief Constructor
40 : CSVFormatter(const std::string& columnNames, const char separator = ';');
41 :
42 : /// @brief Destructor
43 82 : virtual ~CSVFormatter() override { }
44 :
45 : /** @brief Writes an "XML header"
46 : *
47 : * For CSV output the header is only relevant if it contains additional attributes.
48 : *
49 : * @param[in] into The output stream to use
50 : * @param[in] rootElement The root element to use
51 : * @param[in] attrs Additional attributes to save within the rootElement
52 : * @return whether something has been written
53 : */
54 : bool writeXMLHeader(std::ostream& into, const std::string& rootElement,
55 : const std::map<SumoXMLAttr, std::string>& attrs, bool /* writeMetadata */,
56 : bool /* includeConfig */) override;
57 :
58 : /** @brief Keeps track of an open XML tag by adding a new element to the stack
59 : *
60 : * @param[in] into The output stream to use (unused)
61 : * @param[in] xmlElement Name of the element to open
62 : */
63 : void openTag(std::ostream& into, const std::string& xmlElement) override;
64 :
65 : /** @brief Keeps track of an open XML tag by adding a new element to the stack
66 : *
67 : * @param[in] into The output stream to use (unused)
68 : * @param[in] xmlElement Enum identifier of the element to open
69 : */
70 : void openTag(std::ostream& into, const SumoXMLTag& xmlElement) override;
71 :
72 : /** @brief Closes the most recently opened tag
73 : *
74 : * This is where the main action starts. This function determines whether a row is completed and can be written.
75 : *
76 : * @param[in] into The output stream to use
77 : * @param[in] comment A comment to write after the tag (ignored for CSV)
78 : * @return Whether a further element existed in the stack and could be closed
79 : */
80 : bool closeTag(std::ostream& into, const std::string& comment = "") override;
81 :
82 : /** @brief Writes a named attribute
83 : *
84 : * This calls checkAttr to check/add this to the known attributes and then adds the string conversion to myValues.
85 : *
86 : * @param[in] into The output stream to use (used only to determine precision)
87 : * @param[in] attr The attribute (name as enum value)
88 : * @param[in] val The attribute value
89 : * @param[in] isNull whether this is actually a null value (writes the empty string)
90 : * @param[in] escape Whether the value should be processed by StringUtils::escapeCSV (only used in the string variant below)
91 : */
92 : template <class T>
93 102329 : void writeAttr(std::ostream& into, const SumoXMLAttr attr, const T& val, const bool isNull, const bool escape) {
94 : UNUSED_PARAMETER(escape);
95 102329 : checkAttr(attr);
96 102664 : myValues.emplace_back(isNull ? "" : toString(val, into.precision()));
97 102329 : }
98 :
99 : /** @brief Writes a named attribute
100 : *
101 : * This calls checkHeader to check/add this to the known attributes and then adds the string conversion to myValues.
102 : *
103 : * @param[in] into The output stream to use (used only to determine precision)
104 : * @param[in] attr The attribute (name as string)
105 : * @param[in] val The attribute value
106 : * @param[in] isNull whether this is actually a null value (writes the empty string)
107 : * @param[in] escape Whether the value should be processed by StringUtils::escapeCSV (only used in the string variant below)
108 : */
109 : template <class T>
110 32196 : void writeAttr(std::ostream& into, const std::string& attr, const T& val, const bool isNull, const bool escape) {
111 : UNUSED_PARAMETER(escape);
112 : assert(!myCheckColumns);
113 32196 : checkHeader(attr);
114 32196 : myValues.emplace_back(isNull ? "" : toString(val, into.precision()));
115 32196 : }
116 :
117 : /// @brief typed overloads (non-template) -- picked by overload resolution over the template
118 68394 : void writeAttr(std::ostream& /* into */, const SumoXMLAttr attr, const std::string& val, const bool isNull, const bool escape) {
119 68394 : checkAttr(attr);
120 99019 : myValues.emplace_back(isNull ? "" : (escape ? StringUtils::escapeCSV(val, mySeparator) : val));
121 68394 : }
122 20980 : void writeAttr(std::ostream& /* into */, const std::string& attr, const std::string& val, const bool isNull, const bool escape) {
123 : assert(!myCheckColumns);
124 20980 : checkHeader(attr);
125 41528 : myValues.emplace_back(isNull ? "" : (escape ? StringUtils::escapeCSV(val, mySeparator) : val));
126 20980 : }
127 :
128 : /** @brief Writes a time value using time2string
129 : *
130 : * @param[in] into The output stream to use (unused)
131 : * @param[in] attr The attribute (name as enum value)
132 : * @param[in] val The attribute value
133 : */
134 1120 : void writeTime(std::ostream& /* into */, const SumoXMLAttr attr, const SUMOTime val) override {
135 1120 : checkAttr(attr);
136 1120 : myValues.emplace_back(time2string(val));
137 1120 : }
138 :
139 : /** @brief Whether a complete row has been encountered and triggered header writing
140 : *
141 : * @return Whether the CSV header has been written
142 : */
143 0 : bool wroteHeader() const override {
144 0 : return myWroteHeader;
145 : }
146 :
147 : /** @brief Which elements are expected and which maximum depth the XML tree has.
148 : *
149 : * This is not necessary for the functionality but very useful for debugging and tracking whether expected attributes
150 : * are still missing (triggers an error in checkAttr). If expected is empty, no tracking takes place.
151 : *
152 : * The depth parameter is only for performance. If a tag at this depth is closed for the first time,
153 : * the header is being written and buffered rows may be flushed. Setting it to a large value (which is also the default)
154 : * means you are on the safe side if more attributes or tags show up later but it may result in buffering the complete
155 : * output before writing the first line.
156 : * Setting it to 0 triggers auto detection which means the first time a tag is closed the maximum depth will be determined.
157 : *
158 : * @param[in] expected The enum values of the attrs which should be present before a row can be written.
159 : * @param[in] depth The maximum expected depth of nested XML elements.
160 : */
161 28 : void setExpectedAttributes(const SumoXMLAttrMask& expected, const int depth) override {
162 28 : myExpectedAttrs = expected;
163 28 : myMaxDepth = depth;
164 28 : myCheckColumns = expected.any();
165 28 : }
166 :
167 : private:
168 : /** @brief Helper function to keep track of the written attributes and accumulate the header.
169 : * It checks whether the written attribute is expected in the column based format.
170 : * The check does only apply to the deepest level of the XML hierarchy and not to the order of the columns just to the presence.
171 : *
172 : * @param[in] attr The attribute (name as enum value)
173 : */
174 171843 : inline void checkAttr(const SumoXMLAttr attr) {
175 171843 : if (myCheckColumns && myMaxDepth == (int)myXMLStack.size()) {
176 8100 : mySeenAttrs.set(attr);
177 8100 : if (!myExpectedAttrs.test(attr)) {
178 0 : throw ProcessError(TLF("Unexpected attribute '%', this file format does not support CSV output yet.", toString(attr)));
179 : }
180 : }
181 171843 : checkHeader(attr);
182 171843 : }
183 :
184 : template <class ATTR_TYPE>
185 225019 : inline void checkHeader(const ATTR_TYPE& attr) {
186 225019 : myNeedsWrite = true;
187 225019 : if (!myWroteHeader) {
188 157765 : std::string headerName = toString(attr);
189 : std::string prefix;
190 607353 : for (const auto& entry : myXMLStack) {
191 798860 : prefix += entry.first + "_";
192 : }
193 : const std::string fullHeaderName = prefix + headerName;
194 207923 : if (myHeaderFormat != "plain") {
195 623769 : headerName = myXMLStack.back().first + "_" + headerName;
196 : }
197 207923 : const auto colIt = std::find(myFullHeader.begin(), myFullHeader.end(), fullHeaderName);
198 207923 : if (colIt == myFullHeader.end()) {
199 30515 : for (std::string& row : myBufferedRows) {
200 29822 : row += mySeparator;
201 : }
202 693 : myValues.resize(myHeader.size());
203 693 : myHeader.emplace_back(headerName);
204 693 : myFullHeader.emplace_back(fullHeaderName);
205 : } else {
206 : // there might be missing attributes inbetween, so make sure header position and value size match
207 207230 : myValues.resize(std::distance(myFullHeader.begin(), colIt));
208 : }
209 : }
210 225019 : }
211 :
212 : /// @brief the format to use for the column names
213 : const std::string myHeaderFormat;
214 :
215 : /// @brief The value separator
216 : const char mySeparator;
217 :
218 : /// @brief the CSV header
219 : std::vector<std::string> myHeader;
220 :
221 : /// @brief the CSV header if we write the full name
222 : std::vector<std::string> myFullHeader;
223 :
224 : /// @brief The name and number of attributes in the currently open XML elements
225 : std::vector<std::pair<const std::string, int> > myXMLStack;
226 :
227 : /// @brief the current attribute / column values
228 : std::vector<std::string> myValues;
229 :
230 : /// @brief the maximum depth of the XML hierarchy (excluding the root element)
231 : int myMaxDepth = 1000;
232 :
233 : /// @brief whether the CSV header line has been written
234 : bool myWroteHeader = false;
235 :
236 : /// @brief whether any attribute has been written since the last row was emitted
237 : bool myNeedsWrite = false;
238 :
239 : /// @brief whether any root attribute have been encountered
240 : bool myHaveRootAttrs = false;
241 :
242 : /// @brief partial rows buffered before the schema is known (depth < myMaxDepth)
243 : std::vector<std::string> myBufferedRows;
244 :
245 : /// @brief whether the columns should be checked for completeness
246 : bool myCheckColumns = false;
247 :
248 : /// @brief which CSV columns are expected (just for checking completeness)
249 : SumoXMLAttrMask myExpectedAttrs;
250 :
251 : /// @brief which CSV columns have been set (just for checking completeness)
252 : SumoXMLAttrMask mySeenAttrs;
253 : };
|