utils enhanced splitStringByWidth()

Split a word only if there is more than 30% free space on the line.

 - reordered parameters, non-const references first
 - extracted logic to function
This commit is contained in:
silverqx
2024-11-14 17:29:39 +01:00
parent 8c12113899
commit 7399159390
3 changed files with 92 additions and 47 deletions

View File

@@ -96,8 +96,8 @@ namespace Orm::Utils
/*! Split a string by the given width (with or w/o splitting words preference). */
static QStringList
splitStringByWidth(QStringView string, int width,
SplitWordsBehavior splitBehavior = cNeverSplitWords);
splitStringByWidth(QStringView string, int maxWidth,
SplitWordsBehavior splitBehavior = cSplitWords30);
/*! Split a string view at the first given character. */
static QList<QStringView>
splitAtFirst(QStringView string, QChar separator,

View File

@@ -7,7 +7,6 @@
#include <range/v3/view/reverse.hpp>
#include "orm/constants.hpp"
#include "orm/macros/likely.hpp"
TINYORM_BEGIN_COMMON_NAMESPACE
@@ -357,16 +356,82 @@ namespace
constexpr QString::size_type MinFreeSpace = 2;
/*! Push the current line to the lines and start processing a new line. */
void startNewLine(QString &line, QStringList &lines) {
inline void startNewLine(QStringList &lines, QString &line) {
// Push to lines
lines << std::move(line);
// Start a new line
line.clear(); // NOLINT(bugprone-use-after-move), need to clear it anyway even after the move
}
/*! Handle an empty token (edge case). */
inline void handleEmptyToken(QStringList &lines, QString &line, const int width)
{
/* In this case, we have to manually handle the start of a new line because
the splitLongToken() cannot be invoked. */
if (line.size() + 1 > width)
startNewLine(lines, line);
line.append(SPACE);
}
/*! Start a new line or append a space character. */
void startNewLineOrAppendSpace(
QStringList &lines, QString &line, const QStringView token, const int width,
const String::SplitWordsBehavior splitBehavior)
{
/*! Expose the SplitWordsBehavior enum. */
using enum String::SplitWordsBehavior;
// Compute all values only once
const auto tokenSize = token.size();
const auto lineSize = line.size();
const auto freeSpace = width - lineSize;
const auto isTokenOverflow = lineSize + 1 + tokenSize > width; // +1 for prepended space
const auto freeSpaceFactor = width > 34 ? 0.15F : 0.3F;
const auto isFreeSpace30 = freeSpace >= // Is there more than 30% of free space?
std::llround(static_cast<float>(width) *
freeSpaceFactor);
const auto isTokenShorter = tokenSize <= width; // Shorter or equal as the current width
/* If word splitting is not preferred, there must be free space for the entire
token with a space character before; if not, start a new line.
It also helps to avoid maintaining another bool or int state variable
for the following case: Append the token if there is enough free space
on the line, because this case also needs to know if a space character
was appended. */
// The current line is already full (considering also the space character)
if ((lineSize == width || lineSize + 1 == width) ||
(splitBehavior == cNeverSplitWords && isTokenShorter && isTokenOverflow) ||
(splitBehavior == cSplitWords30 && isTokenShorter && isTokenOverflow &&
!isFreeSpace30)
)
return startNewLine(lines, line); // NOLINT(readability-avoid-return-with-void-value) clazy:exclude=returning-void-expression
const auto isTokenLonger = !isTokenShorter; // Longer than the current width
const auto isTokenFit = !isTokenOverflow;
/* Don't append a space character to the beginning of an empty line, and if
there is no free space for at least one more letter when word splitting is
preferred or if is not preferred, there must be free space for the entire
token, but only if the token fits or is smaller than a line; if not, use
the same logic as when word splitting is preferred. 😂😵‍💫🤯
(there is no reason to append a space character in these cases). */
if (const auto lineSizeMinFreeSpace = lineSize + MinFreeSpace;
(splitBehavior == cSplitWords && lineSizeMinFreeSpace <= width) ||
(splitBehavior == cNeverSplitWords &&
((isTokenLonger && lineSizeMinFreeSpace <= width) ||
(isTokenShorter && isTokenFit))) ||
(splitBehavior == cSplitWords30 &&
(((isTokenLonger && lineSizeMinFreeSpace <= width) ||
(isTokenShorter && isTokenFit)) ||
isFreeSpace30))
)
line.append(SPACE);
}
/*! Split the token to multiple lines by the given width. */
void splitLongToken(QStringView token, const int width, QString &line,
QStringList &lines)
void splitLongToken(QStringList &lines, QString &line, QStringView token,
const int width)
{
while (!token.isEmpty()) {
/* Token is shorter than the available free space (occurs when the last part
@@ -384,14 +449,16 @@ namespace
// Cut the currently/above appended token part
token = token.sliced(freeSpace);
startNewLine(line, lines);
startNewLine(lines, line);
}
}
} // namespace
QStringList String::splitStringByWidth(const QStringView string, const int width,
QStringList String::splitStringByWidth(const QStringView string, const int maxWidth,
const SplitWordsBehavior splitBehavior)
{
const auto width = std::max(1, maxWidth);
// Nothing to split
if (string.size() <= width)
return {string.toString()};
@@ -410,53 +477,31 @@ QStringList String::splitStringByWidth(const QStringView string, const int width
// QStringView is trivially copy constructible
for (const auto token : string.split(SPACE, Qt::KeepEmptyParts)) { // clazy:exclude=range-loop-detach
/* If word splitting is not preferred, there must be free space for the entire
token with a space character before; if not, start a new line.
It also helps to avoid maintaining another bool or int state variable
for the following case: Append the token if there is enough free space
on the line, because this case also needs to know if a space character
was appended. */
if (!line.isEmpty() && splitBehavior == cNeverSplitWords &&
token.size() <= width && line.size() + 1 + token.size() > width
)
startNewLine(line, lines);
/* Don't append a space character to the beginning of an empty line, and if
there is no free space for at least one more letter when word splitting is
preferred or if is not preferred, there must be free space for the entire
token, but only if the token fits or is smaller than a line; if not, use
the same logic as when word splitting is preferred. 😂😵‍💫🤯
(there is no reason to append a space character in these cases). */
if (!line.isEmpty() &&
((splitBehavior == cSplitWords && line.size() + MinFreeSpace <= width) ||
(splitBehavior == cNeverSplitWords &&
((token.size() > width && line.size() + MinFreeSpace <= width) ||
(token.size() <= width && line.size() + 1 + token.size() <= width))))
)
line.append(SPACE);
/* Edge case for Qt::KeepEmptyParts, if there are multiple spaces in the row.
In this case, each space character will produce an empty token. */
if (token.isEmpty()) T_UNLIKELY {
/* In this case, we have to manually handle the start of the newline because
the splitLongToken() cannot be invoked. */
if (line.size() + 1 > width)
startNewLine(line, lines);
line.append(SPACE);
if (token.isEmpty()) {
handleEmptyToken(lines, line, width);
continue;
}
// Start a new line or append a space character
if (!line.isEmpty())
startNewLineOrAppendSpace(lines, line, token, width, splitBehavior);
// Append the token if there is enough free space on the line
else if (line.size() + token.size() <= width) T_LIKELY
if (line.size() + token.size() <= width) // line.size() - fresh value needed
line.append(token);
// If the token is longer than the available free space (exceeds the line width)
else
splitLongToken(token, width, line, lines);
splitLongToken(lines, line, token, width);
// The current line is already full (considering also the space character)
if (line.size() == width || line.size() + 1 == width)
startNewLine(line, lines);
/* Extreme edge case when the box width is 1, the line is always empty in this
case, so it cannot be handled above. */
if (width == 1) {
startNewLine(lines, line);
line.append(SPACE);
}
}
// Append the last line processed

View File

@@ -611,12 +611,12 @@ InteractsWithIO::splitStringForErrorWall(const QStringView stringTrimmed) const
lines.reserve(computeReserveForErrorWall(stringSplit, maxLineWidth));
using StringUtils::cNeverSplitWords;
using StringUtils::SplitWordsBehavior::cSplitWords30;
// Split lines by the given width
for (const auto line : stringSplit)
std::ranges::move(
StringUtils::splitStringByWidth(line, maxLineWidth, cNeverSplitWords),
StringUtils::splitStringByWidth(line, maxLineWidth, cSplitWords30),
std::back_inserter(lines));
return lines;