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 "util/DataParser.hpp"
19 : #include "jdx/ParseException.hpp"
20 : #include "util/LdrUtils.hpp"
21 : #include "util/StringUtils.hpp"
22 :
23 : #include <cmath>
24 : #include <limits>
25 : #include <string>
26 :
27 26 : std::vector<double> libjdx::jdx::util::DataParser::readXppYYData(
28 : io::TextReader& reader)
29 : {
30 : static_assert(
31 : std::numeric_limits<double>::has_quiet_NaN, "No quiet NaN available.");
32 :
33 : // read (X++(Y..Y)) data
34 : // TODO: possible performance tweak: yValues.reserve(NPOINTS)
35 26 : std::vector<double> yValues;
36 52 : std::string line;
37 26 : std::streamoff pos = reader.tellg();
38 26 : std::optional<double> yValueCheck = std::nullopt;
39 81 : while (!util::isLdrStart(line = reader.readLine()))
40 : {
41 : // save position to move back if next readLine() encounters LDR start
42 56 : pos = reader.tellg();
43 : // pre-process line
44 112 : auto data = util::stripLineComment(line, true).first;
45 : // read Y values from line
46 111 : auto [lineYValues, isDifEncoded] = readXppYYLine(data, yValueCheck);
47 55 : if (yValueCheck.has_value())
48 : {
49 : // y value is duplicated in new line, trust new value
50 1 : yValues.pop_back();
51 : }
52 : // append line values to yValues
53 : yValues.insert(
54 55 : std::end(yValues), std::begin(lineYValues), std::end(lineYValues));
55 : // if last and second to last values are defined, use last as y check
56 2 : if (!isDifEncoded || lineYValues.empty()
57 2 : || (lineYValues.size() == 1 && std::isnan(lineYValues.back()))
58 59 : || (lineYValues.size() >= 2
59 2 : && (std::isnan(lineYValues.back())
60 2 : || std::isnan(lineYValues[lineYValues.size() - 2]))))
61 : {
62 53 : yValueCheck = std::nullopt;
63 : }
64 : else
65 : {
66 2 : yValueCheck = lineYValues.back();
67 : }
68 : }
69 : // next LDR encountered => all data read => move back to start of next LDR
70 25 : reader.seekg(pos);
71 :
72 50 : return yValues;
73 : }
74 :
75 : std::vector<std::pair<double, double>>
76 14 : libjdx::jdx::util::DataParser::readXyXyData(io::TextReader& reader)
77 : {
78 : static_assert(
79 : std::numeric_limits<double>::has_quiet_NaN, "No quiet NaN available.");
80 :
81 : // read (XY..XY) data
82 14 : std::vector<std::pair<double, double>> xyValues;
83 14 : bool lastValueIsXOnly = false;
84 28 : std::string line;
85 14 : std::streamoff pos = reader.tellg();
86 37 : while (!util::isLdrStart(line = reader.readLine()))
87 : {
88 : // save position to move back if next readLine() encounters LDR start
89 25 : pos = reader.tellg();
90 : // pre-process line
91 50 : auto data = util::stripLineComment(line, true).first;
92 : // read xy values from line
93 50 : auto lineValues = readValues(data, false).first;
94 : // turn line values into pairs and append line values to xyValues
95 120 : for (auto value : lineValues)
96 : {
97 97 : if (lastValueIsXOnly)
98 : {
99 : // must be y value
100 47 : xyValues.back().second = value;
101 47 : lastValueIsXOnly = false;
102 47 : continue;
103 : }
104 : // must be x value
105 50 : if (std::isnan(value))
106 : {
107 : throw ParseException(
108 2 : "NaN value encountered as x value in line: " + line);
109 : }
110 : std::pair<double, double> xyValue{
111 48 : value, std::numeric_limits<double>::quiet_NaN()};
112 48 : lastValueIsXOnly = true;
113 48 : xyValues.push_back(xyValue);
114 : }
115 : }
116 : // next LDR encountered => all data read => move back to start of next LDR
117 12 : reader.seekg(pos);
118 :
119 12 : if (lastValueIsXOnly)
120 : {
121 : // uneven number of single values
122 : throw ParseException("Uneven number of values for xy data "
123 : "encountered. No y value for x value: "
124 1 : + std::to_string(xyValues.back().first));
125 : }
126 22 : return xyValues;
127 : }
128 :
129 : // TODO: refactor to reduce complexity
130 : std::pair<std::vector<double>, bool>
131 : // NOLINTNEXTLINE(readability-function-cognitive-complexity)
132 91 : libjdx::jdx::util::DataParser::readValues(
133 : std::string& encodedValues, bool isAsdf)
134 : {
135 : // output
136 182 : std::vector<double> yValues{};
137 91 : bool difEncoded = false;
138 : // state
139 : // for DIF/DUP previousTokenValue not same as last yValues value
140 91 : std::optional<double> previousTokenValue{};
141 91 : TokenType previousTokenType = TokenType::Affn;
142 : // loop
143 91 : size_t index = 0;
144 487 : while (auto token = nextToken(encodedValues, index, isAsdf))
145 : {
146 396 : TokenType tokenType = toAffn(token.value());
147 : // it's not quite clear if DUP of DIF should also count as DIF encoded
148 : // Bruker seems to think so => apply same logic here
149 396 : difEncoded = tokenType == TokenType::Dif
150 396 : || (difEncoded && tokenType == TokenType::Dup);
151 :
152 : // check for logical errors
153 387 : if ((tokenType == TokenType::Dup || tokenType == TokenType::Dif)
154 783 : && !previousTokenValue.has_value())
155 : {
156 : throw ParseException(
157 : tokenType == TokenType::Dup
158 3 : ? std::string{"DUP"}
159 2 : : std::string{"DIF"}
160 2 : + " token without preceding token encountered in "
161 : "sequence: "
162 3 : + encodedValues);
163 : }
164 9 : if ((tokenType == TokenType::Dup && previousTokenValue.has_value()
165 404 : && previousTokenType == TokenType::Dup))
166 : {
167 : throw ParseException(
168 : "DUP token with preceding DUP token encountered in sequence: "
169 1 : + encodedValues);
170 : }
171 :
172 : // process token
173 394 : if (tokenType == TokenType::Missing)
174 : {
175 : // ?
176 4 : yValues.push_back(std::numeric_limits<double>::quiet_NaN());
177 4 : previousTokenValue = std::numeric_limits<double>::quiet_NaN();
178 : }
179 390 : else if (tokenType == TokenType::Dup)
180 : {
181 8 : auto numRepeats = std::stol(token.value());
182 25 : for (auto i{0}; i < numRepeats - 1; i++)
183 : {
184 17 : if (previousTokenType == TokenType::Dif)
185 : {
186 11 : auto lastValue = yValues.back();
187 11 : auto nextValue = lastValue + previousTokenValue.value();
188 11 : yValues.push_back(nextValue);
189 : }
190 : else
191 : {
192 6 : yValues.push_back(yValues.back());
193 : }
194 : }
195 8 : previousTokenValue = numRepeats;
196 : }
197 : else
198 : {
199 : // TODO: use std::from_chars from <charconv> header
200 : // for better performance when more compilers support it
201 : // see: https://en.cppreference.com/w/cpp/utility/from_chars
202 : // https://gcc.gnu.org/onlinedocs/libstdc++/manual/status.html
203 382 : auto value = std::stod(token.value());
204 382 : if (tokenType == TokenType::Dif)
205 : {
206 64 : if (previousTokenType == TokenType::Missing)
207 : {
208 : throw ParseException("DIF token with preceding ? token "
209 : "encountered in sequence: "
210 0 : + encodedValues);
211 : }
212 64 : auto lastValue = yValues.back();
213 64 : auto nextValue = lastValue + value;
214 64 : yValues.push_back(nextValue);
215 : }
216 : else
217 : {
218 318 : yValues.push_back(value);
219 : }
220 382 : previousTokenValue = value;
221 : }
222 394 : previousTokenType = tokenType;
223 394 : }
224 176 : return {yValues, difEncoded};
225 : }
226 :
227 : std::pair<std::vector<double>, bool>
228 56 : libjdx::jdx::util::DataParser::readXppYYLine(
229 : std::string& line, const std::optional<double>& yValueCheck)
230 : {
231 : // read (X++(Y..Y)) data line
232 112 : auto [values, difEncoded] = readValues(line, true);
233 56 : if (!values.empty())
234 : {
235 : // remove initial x value (not required for (X++(Y..Y)) encoded data)
236 55 : values.erase(values.begin());
237 : // skip X value check
238 : }
239 56 : if (yValueCheck.has_value() && !values.empty())
240 : {
241 : // first y value is a duplicate, check if roughly the same
242 2 : if (fabs(values.front() - yValueCheck.value()) >= 1)
243 : {
244 1 : throw ParseException("Y value check failed in line: " + line);
245 : }
246 : }
247 110 : return {values, difEncoded};
248 : }
249 :
250 798 : std::optional<std::string> libjdx::jdx::util::DataParser::nextToken(
251 : const std::string& line, size_t& pos, bool isAsdf)
252 : {
253 : // skip delimiters
254 798 : while (pos < line.size() && isTokenDelimiter(line, pos))
255 : {
256 313 : pos++;
257 : }
258 485 : if (pos >= line.size())
259 : {
260 88 : return std::nullopt;
261 : }
262 397 : if (!isTokenStart(line, pos, isAsdf))
263 : {
264 1 : throw ParseException("illegal sequence encountered in line \"" + line
265 2 : + "\" at position: " + std::to_string(pos));
266 : }
267 396 : auto startIt = line.cbegin() + static_cast<long>(pos++);
268 1092 : while (!isTokenDelimiter(line, pos) && !isTokenStart(line, pos, isAsdf))
269 : {
270 696 : ++pos;
271 : }
272 396 : auto endIt = line.cbegin() + static_cast<long>(pos);
273 792 : return std::string{startIt, endIt};
274 : }
275 :
276 396 : libjdx::jdx::util::DataParser::TokenType libjdx::jdx::util::DataParser::toAffn(
277 : std::string& token)
278 : {
279 396 : auto c = token.front();
280 396 : TokenType tokenType = TokenType::Affn;
281 396 : std::optional<char> firstDigit;
282 396 : if (c == '?')
283 : {
284 4 : tokenType = TokenType::Missing;
285 : }
286 392 : else if (isSqzDigit(c))
287 : {
288 19 : firstDigit = getSqzDigitValue(c);
289 19 : tokenType = TokenType::Sqz;
290 : }
291 373 : else if (isDifDigit(c))
292 : {
293 65 : firstDigit = getDifDigitValue(c);
294 65 : tokenType = TokenType::Dif;
295 : }
296 308 : else if (isDupDigit(c))
297 : {
298 9 : firstDigit = getDupDigitValue(c);
299 9 : tokenType = TokenType::Dup;
300 : }
301 :
302 396 : if (TokenType::Affn != tokenType && TokenType::Missing != tokenType)
303 : {
304 : // replace SQZ/DIF/DUP char (first char) with (signed) value
305 : // the below code should be faster than the naive:
306 : // token.erase(0, 1);
307 : // token.insert(0, std::to_string(firstDigit.value()));
308 93 : const char value = firstDigit.value();
309 93 : const char digit = value >= 0 ? static_cast<char>('0' + value)
310 24 : : static_cast<char>('0' - value);
311 93 : token[0] = digit;
312 93 : if (value < 0)
313 : {
314 : // TODO: possibly return +/- as separate bool to avoid insert()
315 24 : token.insert(0, "-");
316 : }
317 : }
318 : // must be plain AFFN or PAC (or illegal)
319 396 : return tokenType;
320 : }
321 :
322 2659 : bool libjdx::jdx::util::DataParser::isTokenDelimiter(
323 : const std::string& encodedValues, size_t index)
324 : {
325 2659 : if (index >= encodedValues.size())
326 : {
327 88 : return true;
328 : }
329 2571 : char c = encodedValues[index];
330 2571 : return isSpace(c) || c == ';' || c == ',';
331 : }
332 :
333 1215 : bool libjdx::jdx::util::DataParser::isTokenStart(
334 : const std::string& encodedValues, size_t index, bool isAsdf)
335 : {
336 1215 : if (index >= encodedValues.size())
337 : {
338 0 : return false;
339 : }
340 1215 : char c = encodedValues[index];
341 1649 : if ((isAsciiDigit(c) || c == '.')
342 1649 : && (index == 0 || isTokenDelimiter(encodedValues, index - 1)))
343 : {
344 243 : return true;
345 : }
346 972 : if (c == 'E' || c == 'e')
347 : {
348 : // could be either an exponent or SQZ digit (E==+5, e==-5)
349 : // apply heuristic to provide answer
350 7 : return !isExponentStart(encodedValues, index, isAsdf);
351 : }
352 965 : if (c == '+' || c == '-')
353 : {
354 : // could be either a sign of an exponent or AFFN/PAC start digit
355 : // apply heuristic to provide answer
356 89 : if (index == 0)
357 : {
358 7 : return true;
359 : }
360 82 : return !isExponentStart(encodedValues, index - 1, isAsdf);
361 : }
362 876 : if (isSqzDifDupDigit(c))
363 : {
364 181 : return true;
365 : }
366 695 : if (c == '?')
367 : {
368 : // "invalid" data symbol
369 4 : return true;
370 : }
371 691 : return false;
372 : }
373 :
374 89 : bool libjdx::jdx::util::DataParser::isExponentStart(
375 : const std::string& encodedValues, size_t index, bool isAsdf)
376 : {
377 : // a faster check for start of exponent instead of these regexes:
378 : // ^[eE][+-]{0,1}\\d{1,3}[;,\\s]{1}.*
379 : // ^[eE][+-]{0,1}\\d{1,3}[;,\\s]$
380 : // these heuristics are fragile
381 89 : if (index >= encodedValues.size())
382 : {
383 : throw ParseException(
384 0 : "Illegal index provided for exponent check. Line: " + encodedValues
385 0 : + ", index: " + std::to_string(index));
386 : }
387 89 : auto i = index;
388 89 : auto curChar = encodedValues[i++];
389 89 : if (curChar != 'E' && curChar != 'e')
390 : {
391 81 : return false;
392 : }
393 8 : if (i > encodedValues.size())
394 : {
395 0 : return false;
396 : }
397 8 : curChar = encodedValues[i++];
398 8 : const auto plusMinusFound = curChar == '+' || curChar == '-';
399 8 : if (!plusMinusFound && !isAsciiDigit(curChar))
400 : {
401 0 : return false;
402 : }
403 : // so far, e.g., "E+", "e-", "E2"
404 8 : if (plusMinusFound && i >= encodedValues.size())
405 : {
406 : // string ends with illegal sequence "E+" "E-", "e+", "e-"
407 0 : return false;
408 : }
409 : // no need to update curChar after this point
410 8 : auto numDigits = plusMinusFound ? 0 : 1;
411 15 : while (numDigits < 3 && i < encodedValues.size()
412 30 : && isAsciiDigit(encodedValues[i]))
413 : {
414 7 : ++i;
415 7 : ++numDigits;
416 : }
417 : // so far, e.g., "E+", "E+2", "e-34", "E234"
418 8 : if (numDigits == 0)
419 : {
420 0 : return false;
421 : }
422 : // compressed values can easily end in a line on, e.g., "E567", so require
423 : // delimiter
424 8 : if (isAsdf)
425 : {
426 3 : return i < encodedValues.size() && isTokenDelimiter(encodedValues, i);
427 : }
428 : // for AFFN
429 5 : return i >= encodedValues.size() || isTokenDelimiter(encodedValues, i);
430 : }
431 :
432 1233 : bool libjdx::jdx::util::DataParser::isAsciiDigit(char c)
433 : {
434 1233 : return c >= '0' && c <= '9';
435 : }
436 :
437 392 : bool libjdx::jdx::util::DataParser::isSqzDigit(char c)
438 : {
439 392 : return (c >= '@' && c <= 'I') || (c >= 'a' && c <= 'i');
440 : }
441 :
442 373 : bool libjdx::jdx::util::DataParser::isDifDigit(char c)
443 : {
444 373 : return (c >= 'J' && c <= 'R') || (c >= 'j' && c <= 'r') || c == '%';
445 : }
446 :
447 308 : bool libjdx::jdx::util::DataParser::isDupDigit(char c)
448 : {
449 308 : return (c >= 'S' && c <= 'Z') || c == 's';
450 : }
451 :
452 876 : bool libjdx::jdx::util::DataParser::isSqzDifDupDigit(char c)
453 : {
454 876 : return (c >= '@' && c <= 'Z') || (c >= 'a' && c <= 's') || c == '%';
455 : }
456 :
457 0 : std::optional<char> libjdx::jdx::util::DataParser::getAsciiDigitValue(char c)
458 : {
459 0 : static const std::string asciiDigits = "0123456789";
460 0 : auto pos = asciiDigits.find(c);
461 : return pos == std::string::npos
462 : ? std::nullopt
463 0 : : std::make_optional(static_cast<char>(pos));
464 : }
465 :
466 19 : std::optional<char> libjdx::jdx::util::DataParser::getSqzDigitValue(char c)
467 : {
468 : // positive SQZ digits @ABCDEFGHI
469 19 : if (c >= '@' && c <= 'I')
470 : {
471 16 : return c - '@';
472 : }
473 : // negative SQZ digits abcdefghi
474 3 : if (c >= 'a' && c <= 'i')
475 : {
476 3 : return '`' - c;
477 : }
478 0 : return {};
479 : }
480 :
481 65 : std::optional<char> libjdx::jdx::util::DataParser::getDifDigitValue(char c)
482 : {
483 : // positive DIF digits %JKLMNOPQR
484 65 : if (c == '%')
485 : {
486 10 : return 0;
487 : }
488 55 : if (c >= 'J' && c <= 'R')
489 : {
490 34 : return c - 'I';
491 : }
492 : // negative DIF digits jklmnopqr
493 21 : if (c >= 'j' && c <= 'r')
494 : {
495 21 : return 'i' - c;
496 : }
497 0 : return {};
498 : }
499 :
500 9 : std::optional<char> libjdx::jdx::util::DataParser::getDupDigitValue(char c)
501 : {
502 : // DUP digits STUVWXYZs
503 9 : if (c >= 'S' && c <= 'Z')
504 : {
505 9 : return c - 'R';
506 : }
507 0 : if (c == 's')
508 : {
509 0 : return 9;
510 : }
511 0 : return {};
512 : }
|