Smarter Clipboard Pasting/Parsing (#2706)

This commit is contained in:
Zach H 2017-05-14 14:35:40 -04:00 committed by GitHub
parent 405a719412
commit b53cd33eed
6 changed files with 254 additions and 52 deletions

View file

@ -200,9 +200,19 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklis
QString DeckLoader::getCardZoneFromName(QString cardName, QString currentZoneName) QString DeckLoader::getCardZoneFromName(QString cardName, QString currentZoneName)
{ {
CardInfo *card = db->getCard(cardName); CardInfo *card = db->getCard(cardName);
if(card && card->getIsToken()) if (card && card->getIsToken())
return DECK_ZONE_TOKENS; return DECK_ZONE_TOKENS;
return currentZoneName; return currentZoneName;
} }
QString DeckLoader::getCompleteCardName(const QString cardName) const
{
if (db) {
CardInfo *temp = db->getCardBySimpleName(cardName);
if (temp)
return temp->getName();
}
return cardName;
}

View file

@ -37,6 +37,7 @@ protected:
void saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode); void saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode);
void saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklistNode *zoneNode, QList <DecklistCardNode*> cards); void saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklistNode *zoneNode, QList <DecklistCardNode*> cards);
virtual QString getCardZoneFromName(QString cardName, QString currentZoneName); virtual QString getCardZoneFromName(QString cardName, QString currentZoneName);
virtual QString getCompleteCardName(const QString cardName) const;
}; };
#endif #endif

View file

@ -466,66 +466,111 @@ bool DeckList::saveToFile_Native(QIODevice *device)
bool DeckList::loadFromStream_Plain(QTextStream &in) bool DeckList::loadFromStream_Plain(QTextStream &in)
{ {
cleanList(); cleanList();
QVector<QString> inputs; // QTextStream -> QVector
bool inSideboard = false, isSideboard = false; bool priorEntryIsBlank = true, isAtBeginning = true;
int blankLines = 0;
while (!in.atEnd())
{
QString line = in.readLine().simplified().toLower();
/*
* Removes all blank lines at start of inputs
* Ex: ("", "", "", "Card1", "Card2") => ("Card1", "Card2")
*
* This will also concise multiple blank lines in a row to just one blank
* Ex: ("Card1", "Card2", "", "", "", "Card3") => ("Card1", "Card2", "", "Card3")
*/
if (line.isEmpty()) {
if (priorEntryIsBlank || isAtBeginning)
continue;
priorEntryIsBlank = true;
blankLines++;
} else {
isAtBeginning = false;
priorEntryIsBlank = false;
}
inputs.push_back(line);
}
/*
* Removes blank line at end of inputs (if applicable)
* Ex: ("Card1", "Card2", "") => ("Card1", "Card2")
* NOTE: Any duplicates were taken care of above, so there can be
* at most one blank line at the very end
*/
if (inputs.size() && inputs.last().isEmpty())
{
blankLines--;
inputs.erase(inputs.end() - 1);
}
// If "Sideboard" line appears in inputs, then blank lines mean nothing
if (inputs.contains("sideboard"))
blankLines = 2;
bool inSideboard = false, titleFound = false, isSideboard;
int okRows = 0; int okRows = 0;
bool titleFound = false; foreach(QString line, inputs) {
while (!in.atEnd()) { // This is a comment line, ignore it
QString line = in.readLine().simplified();
// skip comments
if (line.startsWith("//")) if (line.startsWith("//"))
{ {
if(!titleFound) if (!titleFound) { // Set the title to the first comment
{
name = line.mid(2).trimmed(); name = line.mid(2).trimmed();
titleFound = true; titleFound = true;
} else if(okRows == 0) { } else if (okRows == 0) { // We haven't processed any cards yet
comments += line.mid(2).trimmed() + "\n"; comments += line.mid(2).trimmed() + "\n";
} }
continue; continue;
} }
// check for sideboard prefix // If we have a blank line and it's the _ONLY_ blank line in the paste
if (line.startsWith("Sideboard", Qt::CaseInsensitive)) { // Then we assume it means to start the sideboard section of the paste.
// If we have the word "Sideboard" appear on any line, then that will
// also indicate the start of the sideboard.
if ((line.isEmpty() && blankLines == 1) || line.startsWith("sideboard")) {
inSideboard = true; inSideboard = true;
continue; continue; // The line isn't actually a card
} }
isSideboard = inSideboard; isSideboard = inSideboard;
if (line.startsWith("SB:", Qt::CaseInsensitive)) { if (line.startsWith("sb:")) {
line = line.mid(3).trimmed(); line = line.mid(3).trimmed();
isSideboard = true; isSideboard = true;
} }
if (line.trimmed().isEmpty())
continue; // The line was " " instead of "\n"
// Filter out MWS edition symbols and basic land extras // Filter out MWS edition symbols and basic land extras
QRegExp rx("\\[.*\\]"); QRegExp rx("\\[.*\\]");
line.remove(rx); line.remove(rx);
rx.setPattern("\\(.*\\)"); rx.setPattern("\\(.*\\)");
line.remove(rx); line.remove(rx);
//Filter out post card name editions
// Filter out post card name editions
rx.setPattern("\\|.*$"); rx.setPattern("\\|.*$");
line.remove(rx); line.remove(rx);
line = line.simplified();
int i = line.indexOf(' '); int i = line.indexOf(' ');
int cardNameStart = i + 1; int cardNameStart = i + 1;
// If the count ends with an 'x', ignore it. For example, // If the count ends with an 'x', ignore it. For example,
// "4x Storm Crow" will count 4 correctly. // "4x Storm Crow" will count 4 correctly.
if (i > 0 && line[i - 1] == 'x') { if (i > 0 && line[i - 1] == 'x')
i--; i--;
}
bool ok; bool ok;
int number = line.left(i).toInt(&ok); int number = line.left(i).toInt(&ok);
if (!ok) if (!ok)
continue; number = 1; // If input is "cardName" assume it's "1x cardName"
QString cardName = line.mid(cardNameStart); QString cardName = line.mid(cardNameStart);
// Common differences between cockatrice's card names
// Common differences between Cockatrice's card names
// and what's commonly used in decklists // and what's commonly used in decklists
rx.setPattern(""); rx.setPattern("");
cardName.replace(rx, "'"); cardName.replace(rx, "'");
@ -537,20 +582,26 @@ bool DeckList::loadFromStream_Plain(QTextStream &in)
// Replace only if the ampersand is preceded by a non-capital letter, // Replace only if the ampersand is preceded by a non-capital letter,
// as would happen with acronyms. So 'Fire & Ice' is replaced but not // as would happen with acronyms. So 'Fire & Ice' is replaced but not
// 'R&D' or 'R & D'. // 'R&D' or 'R & D'.
// // Qt regexes don't support lookbehind so we capture and replace instead.
// Qt regexes don't support lookbehind so we capture and replace
// instead.
rx.setPattern("([^A-Z])\\s*&\\s*"); rx.setPattern("([^A-Z])\\s*&\\s*");
if (rx.indexIn(cardName) != -1) { if (rx.indexIn(cardName) != -1)
cardName.replace(rx, QString("%1 // ").arg(rx.cap(1))); cardName.replace(rx, QString("%1 // ").arg(rx.cap(1)));
}
// Look for the correct card zone // We need to get the name of the card from the database,
QString zoneName = getCardZoneFromName(cardName, isSideboard ? DECK_ZONE_SIDE: DECK_ZONE_MAIN); // but we can't do that until we get the "real" name
// (name stored in database for the card)
// and establish a card info that is of the card, then it's
// a simple getting the _real_ name of the card
// (i.e. "STOrm, CrOW" => "Storm Crow")
cardName = getCompleteCardName(cardName);
++okRows; // Look for the correct card zone of where to place the new card
QString zoneName = getCardZoneFromName(cardName, isSideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
okRows++;
new DecklistCardNode(cardName, number, getZoneObjFromName(zoneName)); new DecklistCardNode(cardName, number, getZoneObjFromName(zoneName));
} }
updateDeckHash(); updateDeckHash();
return (okRows > 0); return (okRows > 0);
} }

View file

@ -125,6 +125,7 @@ private:
InnerDecklistNode *getZoneObjFromName(const QString zoneName); InnerDecklistNode *getZoneObjFromName(const QString zoneName);
protected: protected:
virtual QString getCardZoneFromName(QString /* cardName */, QString currentZoneName) { return currentZoneName; }; virtual QString getCardZoneFromName(QString /* cardName */, QString currentZoneName) { return currentZoneName; };
virtual QString getCompleteCardName(const QString cardName) const { return cardName; };
signals: signals:
void deckHashChanged(); void deckHashChanged();
public slots: public slots:

View file

@ -39,28 +39,33 @@ struct DecklistBuilder {
}; };
namespace { namespace {
TEST(LoadingFromClipboardTest, EmptyDeck) { TEST(LoadingFromClipboardTest, EmptyDeck)
{
DeckList *deckList = fromClipboard(new QString("")); DeckList *deckList = fromClipboard(new QString(""));
ASSERT_TRUE(deckList->getCardList().isEmpty()) << "Deck should be empty"; ASSERT_TRUE(deckList->getCardList().isEmpty());
} }
TEST(LoadingFromClipboardTest, EmptySideboard) { TEST(LoadingFromClipboardTest, EmptySideboard) {
DeckList *deckList = fromClipboard(new QString("Sideboard")); DeckList *deckList = fromClipboard(new QString("Sideboard"));
ASSERT_TRUE(deckList->getCardList().isEmpty()) << "Deck should be empty"; ASSERT_TRUE(deckList->getCardList().isEmpty());
} }
TEST(LoadingFromClipboardTest, QuantityPrefixed) { TEST(LoadingFromClipboardTest, QuantityPrefixed) {
QString *clipboard = new QString( QString *clipboard = new QString(
"1 Mountain\n" "1 Mountain\n"
"2x Island\n" "2x Island\n"
"3X FOREST\n"
); );
DeckList *deckList = fromClipboard(clipboard); DeckList *deckList = fromClipboard(clipboard);
DecklistBuilder decklistBuilder = DecklistBuilder(); DecklistBuilder decklistBuilder = DecklistBuilder();
deckList->forEachCard(decklistBuilder); deckList->forEachCard(decklistBuilder);
CardRows expectedMainboard = CardRows({{"Mountain", 1}, CardRows expectedMainboard = CardRows({
{"Island", 2}}); {"mountain", 1},
{"island", 2},
{"forest", 3}
});
CardRows expectedSideboard = CardRows({}); CardRows expectedSideboard = CardRows({});
ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard());
@ -70,8 +75,8 @@ namespace {
TEST(LoadingFromClipboardTest, CommentsAreIgnored) { TEST(LoadingFromClipboardTest, CommentsAreIgnored) {
QString *clipboard = new QString( QString *clipboard = new QString(
"//1 Mountain\n" "//1 Mountain\n"
"//2x Island\n" "//2x Island\n"
"//SB:2x Island\n" "//SB:2x Island\n"
); );
DeckList *deckList = fromClipboard(clipboard); DeckList *deckList = fromClipboard(clipboard);
@ -89,17 +94,21 @@ namespace {
TEST(LoadingFromClipboardTest, SideboardPrefix) { TEST(LoadingFromClipboardTest, SideboardPrefix) {
QString *clipboard = new QString( QString *clipboard = new QString(
"1 Mountain\n" "1 Mountain\n"
"SB: 1 Mountain\n" "SB: 1 Mountain\n"
"SB: 2x Island\n" "SB: 2x Island\n"
); );
DeckList *deckList = fromClipboard(clipboard); DeckList *deckList = fromClipboard(clipboard);
DecklistBuilder decklistBuilder = DecklistBuilder(); DecklistBuilder decklistBuilder = DecklistBuilder();
deckList->forEachCard(decklistBuilder); deckList->forEachCard(decklistBuilder);
CardRows expectedMainboard = CardRows({{"Mountain", 1}}); CardRows expectedMainboard = CardRows({
CardRows expectedSideboard = CardRows({{"Mountain", 1}, {"mountain", 1}
{"Island", 2}}); });
CardRows expectedSideboard = CardRows({
{"mountain", 1},
{"island", 2}
});
ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard());
ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard());
@ -114,12 +123,142 @@ namespace {
DecklistBuilder decklistBuilder = DecklistBuilder(); DecklistBuilder decklistBuilder = DecklistBuilder();
deckList->forEachCard(decklistBuilder); deckList->forEachCard(decklistBuilder);
CardRows expectedMainboard = CardRows({{"CardThatDoesNotExistInCardsXml", 1}}); CardRows expectedMainboard = CardRows({
{"cardthatdoesnotexistincardsxml", 1}
});
CardRows expectedSideboard = CardRows({}); CardRows expectedSideboard = CardRows({});
ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard());
ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard());
} }
TEST(LoadingFromClipboardTest, RemoveBlankEntriesFromBeginningAndEnd) {
QString *clipboard = new QString(
"\n"
"\n"
"\n"
"1x Algae Gharial\n"
"3x CardThatDoesNotExistInCardsXml\n"
"2x Phelddagrif\n"
"\n"
"\n"
);
DeckList *deckList = fromClipboard(clipboard);
DecklistBuilder decklistBuilder = DecklistBuilder();
deckList->forEachCard(decklistBuilder);
CardRows expectedMainboard = CardRows({
{"algae gharial", 1},
{"cardthatdoesnotexistincardsxml", 3},
{"phelddagrif", 2}
});
CardRows expectedSideboard = CardRows({});
ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard());
ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard());
}
TEST(LoadingFromClipboardTest, UseFirstBlankIfOnlyOneBlankToSplitSideboard) {
QString *clipboard = new QString(
"1x Algae Gharial\n"
"3x CardThatDoesNotExistInCardsXml\n"
"\n"
"2x Phelddagrif\n"
);
DeckList *deckList = fromClipboard(clipboard);
DecklistBuilder decklistBuilder = DecklistBuilder();
deckList->forEachCard(decklistBuilder);
CardRows expectedMainboard = CardRows({
{"algae gharial", 1},
{"cardthatdoesnotexistincardsxml", 3}
});
CardRows expectedSideboard = CardRows({
{"phelddagrif", 2}
});
ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard());
ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard());
}
TEST(LoadingFromClipboardTest, IfMultipleScatteredBlanksAllMainBoard) {
QString *clipboard = new QString(
"1x Algae Gharial\n"
"3x CardThatDoesNotExistInCardsXml\n"
"\n"
"2x Phelddagrif\n"
"\n"
"3 Giant Growth\n"
);
DeckList *deckList = fromClipboard(clipboard);
DecklistBuilder decklistBuilder = DecklistBuilder();
deckList->forEachCard(decklistBuilder);
CardRows expectedMainboard = CardRows({
{"algae gharial", 1},
{"cardthatdoesnotexistincardsxml", 3},
{"phelddagrif", 2},
{"giant growth", 3}
});
CardRows expectedSideboard = CardRows({});
ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard());
ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard());
}
TEST(LoadingFromClipboardTest, LotsOfStuffInBulkTesting) {
QString *clipboard = new QString(
"\n"
"\n"
"\n"
"1x test1\n"
"testNoValueMB\n"
"2x test2\n"
"SB: 10 testSB\n"
"3 test3\n"
"4X test4\n"
"\n"
"\n"
"\n"
"\n"
"5x test5\n"
"6X test6\n"
"testNoValueSB\n"
"\n"
"\n"
"\n"
"\n"
);
DeckList *deckList = fromClipboard(clipboard);
DecklistBuilder decklistBuilder = DecklistBuilder();
deckList->forEachCard(decklistBuilder);
CardRows expectedMainboard = CardRows({
{"test1", 1},
{"test2", 2},
{"test3", 3},
{"test4", 4},
{"testnovaluemb", 1}
});
CardRows expectedSideboard = CardRows({
{"testsb", 10},
{"test5", 5},
{"test6", 6},
{"testnovaluesb", 1}
});
ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard());
ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard());
}
} }
int main(int argc, char **argv) { int main(int argc, char **argv) {