LCOV - code coverage report
Current view: top level - jdx/util - DataParser.cpp (source / functions) Hit Total Coverage
Test: coverage_filtered.info Lines: 197 214 92.1 %
Date: 2025-06-15 16:18:27 Functions: 17 18 94.4 %

          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             : }

Generated by: LCOV version 1.14