Line data Source code
1 : /* Copyright (C) 2025 Robert Schiwon
2 : *
3 : * This file is part of libjdx.
4 : *
5 : * libjdx is free software: you can redistribute it and/or modify it under the
6 : * terms of the GNU Lesser General Public License as published by the Free
7 : * Software Foundation, either version 3 of the License, or (at your option) any
8 : * later version.
9 : *
10 : * libjdx is distributed in the hope that it will be useful, but WITHOUT ANY
11 : * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 : * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
13 : * details.
14 : *
15 : * You should have received a copy of the GNU Lesser General Public License
16 : * along with libjdx. If not, see <https://www.gnu.org/licenses/>.
17 : */
18 : #include "jdx/NTuples.hpp"
19 : #include "jdx/ParseException.hpp"
20 : #include "util/LdrUtils.hpp"
21 : #include "util/StringUtils.hpp"
22 :
23 : #include <utility>
24 :
25 46 : libjdx::jdx::NTuples::NTuples(const std::string& label, std::string dataForm,
26 : const std::vector<StringLdr>& blockLdrs, io::TextReader& reader,
27 46 : std::optional<std::string>& nextLine)
28 70 : : m_dataForm{std::move(dataForm)}
29 : {
30 46 : validateInput(label);
31 46 : parse(blockLdrs, reader, nextLine);
32 40 : }
33 :
34 13 : std::string libjdx::jdx::NTuples::getDataForm() const
35 : {
36 13 : return m_dataForm;
37 : }
38 :
39 18 : const std::vector<libjdx::jdx::StringLdr>& libjdx::jdx::NTuples::getLdrs() const
40 : {
41 18 : return m_ldrs;
42 : }
43 :
44 : const std::vector<libjdx::jdx::NTuplesAttributes>&
45 16 : libjdx::jdx::NTuples::getAttributes() const
46 : {
47 16 : return m_attributes;
48 : }
49 :
50 11 : size_t libjdx::jdx::NTuples::getNumPages() const
51 : {
52 11 : return m_pages.size();
53 : }
54 :
55 14 : const libjdx::jdx::Page& libjdx::jdx::NTuples::getPage(size_t pageIndex) const
56 : {
57 14 : return m_pages.at(pageIndex);
58 : }
59 :
60 46 : void libjdx::jdx::NTuples::validateInput(const std::string& label)
61 : {
62 46 : if (label != s_label)
63 : {
64 0 : throw ParseException("Illegal label at " + std::string{s_label}
65 0 : + " start encountered: " + label);
66 : }
67 46 : }
68 :
69 46 : void libjdx::jdx::NTuples::parse(const std::vector<StringLdr>& blockLdrs,
70 : io::TextReader& reader, std::optional<std::string>& nextLine)
71 : {
72 : // skip potential comment lines
73 138 : nextLine = reader.eof() ? std::nullopt
74 138 : : std::optional<std::string>{reader.readLine()};
75 46 : util::skipPureComments(reader, nextLine, true);
76 : // parse PAGE parameters
77 46 : m_attributes = parseAttributes(reader, nextLine);
78 : // parse pages
79 112 : while (nextLine.has_value() && util::isLdrStart(nextLine.value()))
80 : {
81 114 : auto [label, pageVar] = util::parseLdrStart(nextLine.value());
82 111 : pageVar = util::stripLineComment(pageVar, true).first;
83 111 : if (label == "ENDNTUPLES")
84 : {
85 : // ##END NTUPLES is described as optional in JCAMP6_2b Draft
86 : // but is required for indicating the NTUPLES end
87 :
88 : // skip ##END NTUPLES
89 40 : nextLine = reader.eof()
90 120 : ? std::nullopt
91 120 : : std::optional<std::string>{reader.readLine()};
92 40 : return;
93 : }
94 71 : if (label != "PAGE")
95 : {
96 : throw ParseException("Unexpected content found in NTUPLES record: "
97 0 : + nextLine.value());
98 : }
99 212 : nextLine = reader.eof() ? std::nullopt
100 212 : : std::optional<std::string>{reader.readLine()};
101 : auto page
102 142 : = Page(label, pageVar, m_attributes, blockLdrs, reader, nextLine);
103 68 : m_pages.push_back(std::move(page));
104 : }
105 1 : if (!nextLine.has_value())
106 : {
107 1 : throw ParseException("Unexpected end of NTUPLES record: " + m_dataForm);
108 : }
109 : }
110 :
111 : std::vector<libjdx::jdx::NTuplesAttributes>
112 46 : libjdx::jdx::NTuples::parseAttributes(
113 : io::TextReader& reader, std::optional<std::string>& nextLine)
114 : {
115 46 : m_ldrs = readLdrs(nextLine, reader);
116 91 : auto attrMap = splitValues(m_ldrs);
117 90 : auto standardAttrMap = extractStandardAttributes(attrMap);
118 :
119 135 : auto attrNames = findValue("VARNAME", standardAttrMap);
120 45 : if (!attrNames)
121 : {
122 : // VARNAMEs are required by the spec
123 : throw ParseException(
124 1 : "No \"VAR_NAME\" LDR found in NTUPLES: " + m_dataForm);
125 : }
126 44 : if (!attrNames.value().empty())
127 : {
128 : // check if last VAR_NAME is blank, i.e., there is a trailing comma
129 : // if so, remove, thus ignore column in subsequent processing
130 : // required to sucessfully process test data set
131 88 : auto lastVarName = attrNames.value().back();
132 44 : util::trim(lastVarName);
133 44 : if (lastVarName.empty())
134 : {
135 12 : attrNames.value().pop_back();
136 : }
137 : }
138 44 : auto numAttrs = attrNames.value().size();
139 :
140 44 : std::vector<NTuplesAttributes> output;
141 198 : for (size_t i = 0; i < numAttrs; ++i)
142 : {
143 308 : auto ntv = map(standardAttrMap, attrMap, i);
144 154 : output.push_back(ntv);
145 : }
146 88 : return output;
147 : }
148 :
149 46 : std::vector<libjdx::jdx::StringLdr> libjdx::jdx::NTuples::readLdrs(
150 : std::optional<std::string>& firstLdrStart, io::TextReader& reader)
151 : {
152 46 : std::optional<std::string>& nextLine = firstLdrStart;
153 46 : std::vector<StringLdr> output;
154 489 : while (nextLine)
155 : {
156 488 : auto [title, value] = util::parseLdrStart(nextLine.value());
157 488 : if (title == "PAGE" || title == "ENDNTUPLES" || title == "END")
158 : {
159 : // all NTUPLES LDRs read
160 45 : break;
161 : }
162 443 : nextLine = parseStringValue(value, reader);
163 443 : output.emplace_back(title, value);
164 : }
165 46 : return output;
166 : }
167 :
168 : std::map<std::string, std::vector<std::string>>
169 46 : libjdx::jdx::NTuples::splitValues(const std::vector<StringLdr>& ldrs)
170 : {
171 46 : std::map<std::string, std::vector<std::string>> output;
172 484 : for (const auto& ldr : ldrs)
173 : {
174 878 : auto valueString = util::stripLineComment(ldr.getValue(), true).first;
175 1317 : auto values = util::split(valueString, ",", true);
176 439 : auto inserted = output.emplace(ldr.getLabel(), values).second;
177 439 : if (!inserted)
178 : {
179 : throw ParseException(
180 1 : "Duplicate LDR found in NTUPLE: " + ldr.getLabel());
181 : }
182 : }
183 45 : return output;
184 : }
185 :
186 : std::map<std::string, std::vector<std::string>>
187 45 : libjdx::jdx::NTuples::extractStandardAttributes(
188 : std::map<std::string, std::vector<std::string>>& attributes)
189 : {
190 : // see:
191 : // https://stackoverflow.com/questions/180516/how-to-filter-items-from-a-stdmap
192 436 : auto matches = [](const std::string& attr) {
193 436 : return std::find(std::begin(s_standardAttrNames),
194 : std::end(s_standardAttrNames), attr)
195 436 : != std::end(s_standardAttrNames);
196 : };
197 45 : std::map<std::string, std::vector<std::string>> standardAttrs;
198 : // remove standard attributes
199 481 : for (auto it = attributes.begin(); it != attributes.end();)
200 : {
201 436 : if (matches((*it).first))
202 : {
203 394 : standardAttrs.insert(*it);
204 394 : attributes.erase(it++);
205 : }
206 : else
207 : {
208 42 : ++it;
209 : }
210 : }
211 90 : return standardAttrs;
212 : }
213 :
214 154 : libjdx::jdx::NTuplesAttributes libjdx::jdx::NTuples::map(
215 : const std::map<std::string, std::vector<std::string>>& standardAttributes,
216 : const std::map<std::string, std::vector<std::string>>& additionalAttributes,
217 : size_t valueColumnIndex)
218 : {
219 : auto pickColumnValue
220 1573 : = [valueColumnIndex](
221 4701 : const std::optional<std::vector<std::string>>& values) {
222 1573 : if (values.value().size() <= valueColumnIndex)
223 : {
224 9 : return std::optional<std::string>{std::nullopt};
225 : }
226 3128 : auto value = values.value().at(valueColumnIndex);
227 1564 : util::trim(value);
228 : return std::optional<std::string>{
229 1564 : values.value().at(valueColumnIndex)};
230 154 : };
231 :
232 : auto findColumnValue
233 1694 : = [&pickColumnValue, &standardAttributes = std::as_const(
234 3255 : standardAttributes)](const std::string& key) {
235 3388 : auto values = findValue(key, standardAttributes);
236 1694 : if (!values)
237 : {
238 287 : return std::optional<std::string>{std::nullopt};
239 : }
240 1407 : return pickColumnValue(values);
241 154 : };
242 :
243 462 : auto varNameString = findColumnValue("VARNAME");
244 154 : if (!varNameString)
245 : {
246 : // VARNAMEs are required by the spec
247 : throw ParseException(
248 0 : R"(No "VAR_NAME" LDR found in NTUPLES ")" + m_dataForm
249 0 : + "\" column: " + std::to_string(valueColumnIndex));
250 : }
251 462 : auto symbolString = findColumnValue("SYMBOL");
252 462 : auto varTypeString = findColumnValue("VARTYPE");
253 462 : auto varFormString = findColumnValue("VARFORM");
254 462 : auto varDimString = findColumnValue("VARDIM");
255 462 : auto unitsString = findColumnValue("UNITS");
256 462 : auto firstString = findColumnValue("FIRST");
257 462 : auto lastString = findColumnValue("LAST");
258 462 : auto minString = findColumnValue("MIN");
259 462 : auto maxString = findColumnValue("MAX");
260 462 : auto factorString = findColumnValue("FACTOR");
261 :
262 : // TODO: make stol/stod more reliable (new util function) and use across
263 : // project
264 154 : if (!varNameString)
265 : {
266 : throw ParseException("VAR_NAME missing in NTUPLES column: "
267 0 : + std::to_string(valueColumnIndex));
268 : }
269 154 : if (!symbolString)
270 : {
271 : throw ParseException("SYMBOL missing in NTUPLES column: "
272 0 : + std::to_string(valueColumnIndex));
273 : }
274 308 : auto varName = varNameString.value();
275 308 : auto symbol = symbolString.value();
276 154 : auto& varType = varTypeString;
277 154 : auto& varForm = varFormString;
278 :
279 296 : auto varDim = varDimString && !varDimString.value().empty()
280 270 : ? std::optional<uint64_t>(std::stol(varDimString.value()))
281 154 : : std::nullopt;
282 154 : auto& units = unitsString;
283 283 : auto first = firstString && !firstString.value().empty()
284 255 : ? std::optional<double>(std::stod(firstString.value()))
285 154 : : std::nullopt;
286 283 : auto last = lastString && !lastString.value().empty()
287 255 : ? std::optional<double>(std::stod(lastString.value()))
288 154 : : std::nullopt;
289 238 : auto min = minString && !minString.value().empty()
290 238 : ? std::optional<double>(std::stod(minString.value()))
291 154 : : std::nullopt;
292 238 : auto max = maxString && !maxString.value().empty()
293 238 : ? std::optional<double>(std::stod(maxString.value()))
294 154 : : std::nullopt;
295 245 : auto factor = factorString && !factorString.value().empty()
296 245 : ? std::optional<double>(std::stod(factorString.value()))
297 154 : : std::nullopt;
298 :
299 308 : std::vector<StringLdr> additionalAttrsInColum;
300 320 : for (const auto& var : additionalAttributes)
301 : {
302 332 : auto value = pickColumnValue(var.second);
303 166 : if (value)
304 : {
305 161 : additionalAttrsInColum.emplace_back(var.first, value.value());
306 : }
307 : }
308 :
309 : NTuplesAttributes nTupleAttrs{
310 : varName,
311 : symbol,
312 : varType,
313 : varForm,
314 : varDim,
315 : units,
316 : first,
317 : last,
318 : min,
319 : max,
320 : factor,
321 : additionalAttrsInColum,
322 154 : };
323 :
324 308 : return nTupleAttrs;
325 : }
326 :
327 1739 : std::optional<std::vector<std::string>> libjdx::jdx::NTuples::findValue(
328 : const std::string& key,
329 : const std::map<std::string, std::vector<std::string>>& map)
330 : {
331 1739 : const auto it = map.find(key);
332 1739 : if (it != map.end())
333 : {
334 1451 : return std::optional<std::vector<std::string>>{(*it).second};
335 : }
336 288 : return std::optional<std::vector<std::string>>{std::nullopt};
337 : }
|