Improve parser

This commit is contained in:
Loic Guegan 2022-01-25 16:51:37 +01:00
parent 773d93b02e
commit e063e1453c
4 changed files with 77 additions and 20 deletions

View file

@ -3,6 +3,10 @@
PGNP is a Portable Game Notation (PGN) parser. More details about the
PGN specification can be found [here](https://www.chessclub.com/help/PGN-spec).
# Features
- Basic PGN parsing (tags, move, comments, variations etc.)
- Merged PGN files parsing (several games in one file)
# How to use it ?
PGNP can be used as a shared library in your project.
You only need to include `pgnp.hpp` and linking the .so file to your
@ -17,6 +21,7 @@ Load PGN from file:
pgnp::PGN pgn;
try {
pgn.FromFile("pgn.txt");
pgn.ParseNextGame();
}
catch(...){
// Handle exceptions
@ -24,8 +29,9 @@ Load PGN from file:
Load PGN from string:
pgnp::PGN pgn;
pgn.FromString("YOUR PGN CONTENT HERE");
try {
pgn.FromString("YOUR PGN CONTENT HERE");
pgn.ParseNextGame();
}
catch(...){
// Handle exceptions

View file

@ -7,7 +7,7 @@
#define IS_DIGIT(c) \
(c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || c == '5' || \
c == '6' || c == '7' || c == '8' || c == '9')
#define IS_EOF(loc) (loc >= pgn_content.size())
#define IS_EOF(loc) ((loc) >= pgn_content.size())
#define EOF_CHECK(loc) \
{ \
if (IS_EOF(loc)) \
@ -16,6 +16,8 @@
namespace pgnp {
PGN::PGN() : LastGameEndLoc(0), moves(NULL) {}
PGN::~PGN() {
if (moves != NULL)
delete moves;
@ -26,15 +28,30 @@ std::string PGN::GetResult() { return (result); }
void PGN::FromFile(std::string filepath) {
std::ifstream file(filepath);
std::string content((std::istreambuf_iterator<char>(file)),
std::string pgn_content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
FromString(content);
this->pgn_content = pgn_content;
}
void PGN::FromString(std::string pgn_content) {
this->pgn_content = pgn_content;
}
void PGN::ParseNextGame(){
// Clean previous parse
if(moves!=NULL){
delete moves;
}
result="";
tagkeys.clear();
tags.clear();
moves = new HalfMove();
int loc = 0;
int loc = NextNonBlank(LastGameEndLoc);
if(IS_EOF(loc)){
throw NoGameFound();
}
while (!IS_EOF(loc)) {
char c = pgn_content[loc];
if (!IS_BLANK(c)) {
@ -42,9 +59,10 @@ void PGN::FromString(std::string pgn_content) {
loc = ParseNextTag(loc);
} else if (IS_DIGIT(c)) {
loc = ParseHalfMove(loc, moves);
LastGameEndLoc=loc+1; // Next game start 1 char after the last one
break;
} else if (c=='{') {
loc = ParseComment(loc,moves);
} else if (c == '{') {
loc = ParseComment(loc, moves);
continue; // No need loc++
}
}
@ -89,20 +107,20 @@ int PGN::ParseComment(int loc, HalfMove *hm) {
loc = NextNonBlank(loc);
EOF_CHECK(loc);
char c = pgn_content[loc];
if(c=='{'){
if (c == '{') {
loc++;
EOF_CHECK(loc);
c = pgn_content[loc];
while(c!='}'){
hm->comment+=c;
while (c != '}') {
hm->comment += c;
loc++;
EOF_CHECK(loc);
c = pgn_content[loc];
}
loc++; // Skip '}'
}
return(loc);
return (loc);
}
int PGN::ParseHalfMove(int loc, HalfMove *hm) {
@ -120,11 +138,14 @@ int PGN::ParseHalfMove(int loc, HalfMove *hm) {
} else if (nc == '-') {
if (c == '1') {
result = "1-0";
loc+=2;
} else {
result = "0-1";
loc+=2;
}
} else {
result = "1/2-1/2";
loc+=6;
}
return (loc);
}
@ -151,8 +172,9 @@ int PGN::ParseHalfMove(int loc, HalfMove *hm) {
hm->isBlack = true;
}
// Parse comment entries (various comment could appear during HalfMove parsing)
loc=ParseComment(loc,hm);
// Parse comment entries (various comment could appear during HalfMove
// parsing)
loc = ParseComment(loc, hm);
// Parse the HalfMove
loc = NextNonBlank(loc);
@ -168,7 +190,7 @@ int PGN::ParseHalfMove(int loc, HalfMove *hm) {
hm->move = move;
// Parse comment
loc=ParseComment(loc,hm);
loc = ParseComment(loc, hm);
// Skip end of variation
if (c == ')') {
@ -177,7 +199,7 @@ int PGN::ParseHalfMove(int loc, HalfMove *hm) {
}
// Parse comment
loc=ParseComment(loc,hm);
loc = ParseComment(loc, hm);
// Check for variations
loc = NextNonBlank(loc);
@ -190,7 +212,7 @@ int PGN::ParseHalfMove(int loc, HalfMove *hm) {
}
// Parse comment
loc=ParseComment(loc,hm);
loc = ParseComment(loc, hm);
// Parse next HalfMove
loc = NextNonBlank(loc);

View file

@ -14,15 +14,23 @@ private:
std::vector<std::string> tagkeys;
/// @brief Contains game result (last PGN word)
std::string result;
/// @brief COntains the parsed PGN moves
/// @brief Contains the parsed PGN moves
HalfMove *moves;
/// @brief Contains the PGN data
std::string pgn_content;
/// @brief Contains the location of the end of the last parsed game (1 PGN file may have multiple games)
int LastGameEndLoc;
public:
PGN();
~PGN();
void FromFile(std::string);
void FromString(std::string);
/**
* Parse the next available game. Note that it raises a @a NoGameFound exception if no more game is available.
* A call to this method flush all the last parsed game data. Be careful.
*/
void ParseNextGame();
/// @brief Check if PGN contains a specific tag
bool HasTag(std::string);
/// @brief Perform a Seven Tag Roster compliance check
@ -62,6 +70,10 @@ struct InvalidGameResult : public std::exception {
const char *what() const throw() { return "Invalid game result"; }
};
struct NoGameFound : public std::exception {
const char *what() const throw() { return "No game (or more game) found"; }
};
struct UnexpectedCharacter : public std::exception {
std::string msg;
UnexpectedCharacter(char actual, char required, int loc) {

View file

@ -6,6 +6,7 @@ using namespace pgnp;
TEST_CASE("Valid PGN", "[valid/pgn1]") {
PGN pgn;
REQUIRE_NOTHROW(pgn.FromFile("pgn_files/valid/pgn1.pgn"));
REQUIRE_NOTHROW(pgn.ParseNextGame());
REQUIRE_THROWS(pgn.STRCheck());
HalfMove *m = new HalfMove();
@ -33,7 +34,7 @@ TEST_CASE("Valid PGN", "[valid/pgn1]") {
}
SECTION("Main line color checks") {
m=m_backup;
m = m_backup;
CHECK_FALSE(m->isBlack);
m = m->MainLine;
@ -60,26 +61,42 @@ TEST_CASE("Valid PGN", "[valid/pgn1]") {
CHECK(m_backup->GetHalfMoveAt(4)->move == "c4");
CHECK(pgn.GetResult() == "*");
REQUIRE_THROWS_AS(pgn.ParseNextGame(),NoGameFound);
}
TEST_CASE("Valid PGN", "[valid/pgn2]") {
PGN pgn;
REQUIRE_NOTHROW(pgn.FromFile("pgn_files/valid/pgn2.pgn"));
REQUIRE_NOTHROW(pgn.ParseNextGame());
REQUIRE_THROWS(pgn.STRCheck());
HalfMove *m = new HalfMove();
pgn.GetMoves(m);
REQUIRE(m->GetLength() == 66);
CHECK(pgn.GetResult() == "0-1");
CHECK(m->comment == " A00 Hungarian Opening ");
CHECK(m->GetHalfMoveAt(7)->comment == " (0.22 → 0.74) Inaccuracy. dxc4 was best. ");
CHECK(m->GetHalfMoveAt(65)->comment == " White resigns. ");
CHECK(m->GetHalfMoveAt(7)->comment ==
" (0.22 → 0.74) Inaccuracy. dxc4 was best. ");
SECTION("Check Variations") {
HalfMove *var = m->GetHalfMoveAt(7)->variations[0];
REQUIRE(var->GetLength() == 10);
CHECK(var->move == "dxc4");
CHECK(var->GetHalfMoveAt(1)->move == "O-O");
}
REQUIRE_THROWS_AS(pgn.ParseNextGame(),NoGameFound);
}
TEST_CASE("Seven Tag Roster", "[std/pgn1]") {
PGN pgn;
REQUIRE_NOTHROW(pgn.FromFile("pgn_files/str/pgn1.pgn"));
REQUIRE_NOTHROW(pgn.ParseNextGame());
REQUIRE_NOTHROW(pgn.STRCheck());
HalfMove *m = new HalfMove();
pgn.GetMoves(m);
REQUIRE(m->GetLength() == 85);
CHECK(pgn.GetResult() == "1/2-1/2");
REQUIRE_THROWS_AS(pgn.ParseNextGame(),NoGameFound);
}