[LIB-9] Add SAN, NAG, move number and result PGN tokens
authorgotty <gotty@hedgecode.org>
Mon, 27 Jan 2020 00:39:41 +0000 (03:39 +0300)
committergotty <gotty@hedgecode.org>
Mon, 27 Jan 2020 00:39:41 +0000 (03:39 +0300)
22 files changed:
chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNConstants.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNUtils.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailGame.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailMove.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/MoveNumber.java [new file with mode: 0644]
chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Moves.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Variation.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/format/ExportMovesFormat.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/nag/Glyph.java [new file with mode: 0644]
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/EmptyToken.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveNumberToken.java [new file with mode: 0644]
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveToken.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MovesToken.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/ResultToken.java [new file with mode: 0644]
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/TagToken.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/Tokenizer.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/VariationToken.java
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/CommonSANToken.java [new file with mode: 0644]
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/LegalSANToken.java [new file with mode: 0644]
chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/SANToken.java [new file with mode: 0644]
chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings.properties
chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings_ru.properties

index 45b2408..c0e3660 100644 (file)
@@ -35,6 +35,7 @@ public final class PGNConstants {
     public static final String BLACK_MOVE_FORMAT = "%s";
     public static final String BLACK_MOVE_DOT_FORMAT = "%d...%s";
 
+    public static final String NAG_FORMAT = "$%d";
     public static final String COMMENT_FORMAT = "{%s}";
     public static final String VARIATION_FORMAT = "(%s)";
 
index 822c31f..884a388 100644 (file)
@@ -19,6 +19,8 @@ package org.hedgecode.chess.pgn;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static org.hedgecode.chess.pgn.PGNConstants.*;
+
 /**
  * PGNUtils
  *
@@ -29,8 +31,8 @@ public final class PGNUtils {
     private static final char BACKSLASH = '\\';
     private static final String SHIELD_REGEX = "\\\\";
 
-    private static final String CRLF = "\\r?\\n";
-    private static final String SPACE = " ";
+    private static final String EMPTY = "";
+    private static final String CRLF_REGEX = "\\r?\\n";
 
     public static String match(String source, String regex) {
         Matcher matcher = Pattern.compile(
@@ -48,7 +50,7 @@ public final class PGNUtils {
     public static boolean isPgn(String source) {
         return match(
                 source,
-                PGNConstants.PGN_DETECT_REGEX
+                PGN_DETECT_REGEX
         ) != null;
     }
 
@@ -68,8 +70,16 @@ public final class PGNUtils {
         return source;
     }
 
+    public static boolean isEmpty(String pgn) {
+        return pgn.replaceAll(CRLF_REGEX, EMPTY).isEmpty();
+    }
+
+    public static String nextLine(String pgn) {
+        return pgn.substring(0, pgn.indexOf(PGN_CRLF) + 1);
+    }
+
     public static String stripCrlf(String pgn) {
-        return pgn.replaceAll(CRLFSPACE);
+        return pgn.replaceAll(CRLF_REGEX, PGN_SPACE);
     }
 
     private PGNUtils() {
index 7486dc0..c80eb72 100644 (file)
@@ -21,6 +21,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.hedgecode.chess.ParseException;
+
 /**
  * DetailGame
  *
@@ -51,6 +53,11 @@ public class DetailGame implements Game, Moves {
     }
 
     @Override
+    public Moves parent() {
+        return null;
+    }
+
+    @Override
     public DetailMove nullMove() {
         return NULL_MOVE;
     }
@@ -61,6 +68,21 @@ public class DetailGame implements Game, Moves {
     }
 
     @Override
+    public void setResult(String result) throws ParseException {
+        String tagResult = tags.get(Tag.RESULT.getName());
+        if (tagResult == null) {
+            tags.put(Tag.RESULT.getName(), result);
+        } else if (!tagResult.equals(result)) {
+            throw new ParseException("parse.pgn.result.not.match");
+        }
+    }
+
+    @Override
+    public String result() {
+        return tags.get(Tag.RESULT.getName());
+    }
+
+    @Override
     public void addTag(String name, String value) {
         tags.put(name, value);
     }
index 2b37204..4463a77 100644 (file)
@@ -19,6 +19,8 @@ package org.hedgecode.chess.pgn.entity;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.hedgecode.chess.pgn.nag.Glyph;
+
 /**
  * DetailMove
  *
@@ -29,6 +31,7 @@ public class DetailMove implements Move {
     private int ply;
     private String move;
 
+    private List<Glyph> glyphs = new ArrayList<>();
     private List<String> comments = new ArrayList<>();
     private List<Variation> variations = new ArrayList<>();
 
@@ -47,6 +50,10 @@ public class DetailMove implements Move {
         return move;
     }
 
+    public void addGlyph(Glyph glyph) {
+        glyphs.add(glyph);
+    }
+
     public void addComment(String comment) {
         comments.add(comment);
     }
@@ -55,6 +62,10 @@ public class DetailMove implements Move {
         variations.add(variation);
     }
 
+    public List<Glyph> glyphs() {
+        return glyphs;
+    }
+
     public List<String> comments() {
         return comments;
     }
diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/MoveNumber.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/MoveNumber.java
new file mode 100644 (file)
index 0000000..0ba4571
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018-2020. Developed by Hedgecode.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hedgecode.chess.pgn.entity;
+
+/**
+ * MoveNumber
+ *
+ * @author Dmitry Samoshin aka gotty
+ */
+public class MoveNumber {
+
+    private int ply;
+
+    public MoveNumber() {
+        this(0);
+    }
+
+    public MoveNumber(int prevPly) {
+        this.ply = prevPly + 1;
+    }
+
+    public void assignNumber(int number, boolean isBlack) {
+        this.ply = number * 2 - (isBlack ? 0 : 1);
+    }
+
+    public int ply() {
+        return ply;
+    }
+
+    public int number() {
+        return ply % 2 != 0
+                ? ply / 2 + 1
+                : ply / 2;
+    }
+
+}
index c751132..36704a3 100644 (file)
@@ -18,6 +18,8 @@ package org.hedgecode.chess.pgn.entity;
 
 import java.util.List;
 
+import org.hedgecode.chess.ParseException;
+
 /**
  * Moves
  *
@@ -29,8 +31,14 @@ public interface Moves {
 
     List<DetailMove> getMoves();
 
+    Moves parent();
+
     DetailMove nullMove();
 
     DetailMove currentMove();
 
+    void setResult(String result) throws ParseException;
+
+    String result();
+
 }
index 3c290e8..c7a8c9a 100644 (file)
@@ -19,6 +19,8 @@ package org.hedgecode.chess.pgn.entity;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.hedgecode.chess.ParseException;
+
 /**
  * Variation
  *
@@ -28,11 +30,14 @@ public class Variation implements Moves {
 
     private final DetailMove NULL_MOVE = new DetailMove(0, null);
 
+    private Moves parentMoves;
+
     private DetailMove currentMove;
     private final List<DetailMove> moves = new ArrayList<>();
 
-    public Variation() {
-        currentMove = NULL_MOVE;
+    public Variation(Moves parentMoves) {
+        this.parentMoves = parentMoves;
+        this.currentMove = NULL_MOVE;
     }
 
     @Override
@@ -47,6 +52,11 @@ public class Variation implements Moves {
     }
 
     @Override
+    public Moves parent() {
+        return parentMoves;
+    }
+
+    @Override
     public DetailMove nullMove() {
         return NULL_MOVE;
     }
@@ -56,4 +66,14 @@ public class Variation implements Moves {
         return currentMove;
     }
 
+    @Override
+    public void setResult(String result) throws ParseException {
+        throw new ParseException("parse.pgn.variation.result");
+    }
+
+    @Override
+    public String result() {
+        return null;
+    }
+
 }
index 4de3b41..404f8da 100644 (file)
@@ -23,6 +23,7 @@ import org.hedgecode.chess.pgn.entity.DetailMove;
 import org.hedgecode.chess.pgn.entity.Move;
 import org.hedgecode.chess.pgn.entity.Moves;
 import org.hedgecode.chess.pgn.entity.Variation;
+import org.hedgecode.chess.pgn.nag.Glyph;
 
 import static org.hedgecode.chess.pgn.PGNConstants.*;
 
@@ -103,6 +104,8 @@ public class ExportMovesFormat extends AbstractMovesFormat {
     private String formatAddition(DetailMove move) {
         StringBuilder sb = new StringBuilder();
         sb.append(
+                formatNag(move)
+        ).append(
                 formatComment(move)
         ).append(
                 formatVariation(move)
@@ -110,6 +113,16 @@ public class ExportMovesFormat extends AbstractMovesFormat {
         return sb.toString();
     }
 
+    private String formatNag(DetailMove move) {
+        StringBuilder sb = new StringBuilder();
+        for (Glyph glyph : move.glyphs()) {
+            sb.append(
+                    String.format(NAG_FORMAT, glyph.number())
+            ).append(PGN_SPACE);
+        }
+        return sb.toString();
+    }
+
     private String formatComment(DetailMove move) {
         StringBuilder sb = new StringBuilder();
         for (String comment : move.comments()) {
diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/nag/Glyph.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/nag/Glyph.java
new file mode 100644 (file)
index 0000000..b343300
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2018-2020. Developed by Hedgecode.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hedgecode.chess.pgn.nag;
+
+/**
+ * Numeric Annotation Glyph (NAG) Enum.
+ *
+ * @author Dmitry Samoshin aka gotty
+ */
+public enum Glyph {
+
+    NULL               ( 0, null, "null annotation" ),
+    GOOD_MOVE          ( 1, "!",  "good move" ),
+    POOR_MOVE          ( 2, "?",  "poor move" ),
+    VERY_GOOD_MOVE     ( 3, "!!", "very good move" ),
+    VERY_POOR_MOVE     ( 4, "??", "very poor move" ),
+    SPECULATIVE_MOVE   ( 5, "!?", "speculative move" ),
+    QUESTIONABLE_MOVE  ( 6, "?!", "questionable move" );
+    // todo: ...
+
+    private int number;
+    private String symbol;
+    private String interpretation;
+
+    Glyph(int number, String symbol, String interpretation) {
+        this.number = number;
+        this.symbol = symbol;
+        this.interpretation = interpretation;
+    }
+
+    public int number() {
+        return number;
+    }
+
+    public String symbol() {
+        return symbol;
+    }
+
+    public String interpretation() {
+        return interpretation;
+    }
+
+    public static Glyph byNumber(int number) {
+        for (Glyph glyph : Glyph.values()) {
+            if (glyph.number == number) {
+                return glyph;
+            }
+        }
+        return null;
+    }
+
+    public static Glyph bySymbol(String symbol) {
+        if (symbol == null) return NULL;
+        for (Glyph glyph : Glyph.values()) {
+            if (symbol.equals(glyph.symbol)) {
+                return glyph;
+            }
+        }
+        return null;
+    }
+
+}
index 2fcbc86..082e3d7 100644 (file)
 
 package org.hedgecode.chess.pgn.token;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
 import org.hedgecode.chess.ParseException;
-import org.hedgecode.chess.pgn.PGNConstants;
+import org.hedgecode.chess.pgn.PGNUtils;
 import org.hedgecode.chess.pgn.entity.Game;
 
 /**
@@ -30,16 +27,13 @@ import org.hedgecode.chess.pgn.entity.Game;
  */
 public class EmptyToken implements Token<Game> {
 
-    private static final String EMPTY_REGEX = "^\\s*$";
-    private static final Pattern EMPTY_PATTERN = Pattern.compile(EMPTY_REGEX, Pattern.MULTILINE);
-
     @Override
     public int token(Game game, String pgn) throws ParseException {
-        Matcher matcher = EMPTY_PATTERN.matcher(pgn);
-        if (!matcher.find()) {
+        String empty = PGNUtils.nextLine(pgn);
+        if (!PGNUtils.isEmpty(empty)) {
             throw new ParseException("parse.pgn.incorrect.empty", pgn);
         }
-        return pgn.indexOf(PGNConstants.PGN_CRLF) + 1;
+        return empty.length();
     }
 
 }
diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveNumberToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveNumberToken.java
new file mode 100644 (file)
index 0000000..f958ba2
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018-2020. Developed by Hedgecode.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hedgecode.chess.pgn.token;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.hedgecode.chess.ParseException;
+import org.hedgecode.chess.pgn.entity.MoveNumber;
+
+/**
+ * MoveNumberToken
+ *
+ * @author Dmitry Samoshin aka gotty
+ */
+public class MoveNumberToken implements Token<MoveNumber> {
+
+    private static final String MOVE_NUMBER_REGEX = "^\\s*([0-9]+)\\s*([.]{1,3})\\s*";
+    private static final Pattern MOVE_NUMBER_PATTERN = Pattern.compile(MOVE_NUMBER_REGEX);
+    private static final int NUM_GROUP = 1, DOT_GROUP = 2;
+    private static final int BLACK_DOT_LENGTH = 3;
+
+    @Override
+    public int token(MoveNumber moveNumber, String pgn) throws ParseException {
+        Matcher matcher = MOVE_NUMBER_PATTERN.matcher(pgn);
+        if (matcher.find()) {
+            int number = Integer.parseInt(matcher.group(NUM_GROUP));
+            boolean isBlack = matcher.group(DOT_GROUP).length() == BLACK_DOT_LENGTH;
+            moveNumber.assignNumber(
+                    number, isBlack
+            );
+            return matcher.group().length();
+        }
+        return 0;
+    }
+
+}
index de59a2c..ea89dfc 100644 (file)
@@ -21,7 +21,11 @@ import java.util.regex.Pattern;
 
 import org.hedgecode.chess.ParseException;
 import org.hedgecode.chess.pgn.entity.DetailMove;
+import org.hedgecode.chess.pgn.entity.MoveNumber;
 import org.hedgecode.chess.pgn.entity.Moves;
+import org.hedgecode.chess.pgn.nag.Glyph;
+import org.hedgecode.chess.pgn.token.san.CommonSANToken;
+import org.hedgecode.chess.pgn.token.san.SANToken;
 
 /**
  * MoveToken
@@ -30,32 +34,44 @@ import org.hedgecode.chess.pgn.entity.Moves;
  */
 public class MoveToken implements Token<Moves> {
 
-    private static final String MOVE_REGEX = "^\\s*(([0-9]+)\\s*(\\.|\\.{3})\\s*)*([PNBRQK]?[a-hx1-8\\-O]+(=[NBRQ])?([+#])?)";
+    public static final String MOVE_REGEX = "^\\s*([PNBRQKa-hx1-8=+#\\-O!?]+)(\\s+\\$([0-9]+))?";
     private static final Pattern MOVE_PATTERN = Pattern.compile(MOVE_REGEX);
-    private static final int PLY_GROUP = 2, DOT_GROUP = 3, MOVE_GROUP = 4;
-    private static final String DOT_BLACK = "...";
+    private static final int MOVE_GROUP = 1, NAG_GROUP = 3;
+
+    private final Token<MoveNumber> moveNumberToken = new MoveNumberToken();
+    private final SANToken moveSanToken = new CommonSANToken();
 
     @Override
     public int token(Moves moves, String pgn) throws ParseException {
+        MoveNumber moveNumber = new MoveNumber(
+                moves.currentMove().ply()
+        );
+        int numberLength = moveNumberToken.token(moveNumber, pgn);
+        if (numberLength > 0) {
+            pgn = pgn.substring(numberLength);
+        }
         Matcher matcher = MOVE_PATTERN.matcher(pgn);
         if (!matcher.find()) {
             throw new ParseException("parse.pgn.incorrect.move", pgn);
         } else {
-            boolean isBlackMove = isBlackMove(matcher.group(DOT_GROUP));
-            String plyStr = matcher.group(PLY_GROUP);
-            int ply = plyStr != null
-                    ? Integer.parseInt(plyStr) * 2 - (isBlackMove ? 0 : 1)
-                    : moves.currentMove().ply() + 1;
             String move = matcher.group(MOVE_GROUP);
+            moveSanToken.token(move);
+            DetailMove detailMove = new DetailMove(
+                    moveNumber.ply(), move
+            );
             moves.addMove(
-                    new DetailMove(ply, move)
+                    detailMove
             );
+            String glyph = matcher.group(NAG_GROUP); // todo: more then one NAG
+            if (glyph != null) {
+                detailMove.addGlyph(
+                        Glyph.byNumber(
+                                Integer.parseInt(glyph)
+                        )
+                );
+            }
         }
-        return matcher.group().length();
-    }
-
-    private boolean isBlackMove(String dotGroup) {
-        return dotGroup == null || dotGroup.equals(DOT_BLACK);
+        return numberLength + matcher.group().length();
     }
 
 }
index e766190..78de96c 100644 (file)
@@ -34,7 +34,8 @@ public class MovesToken<Entity extends Moves> implements Token<Entity> {
 
         MOVE      ( new MoveToken()      ),
         COMMENT   ( new CommentToken()   ),
-        VARIATION ( new VariationToken() );
+        VARIATION ( new VariationToken() ),
+        RESULT    ( new ResultToken()    );
 
         private Token<Moves> token;
 
@@ -50,38 +51,50 @@ public class MovesToken<Entity extends Moves> implements Token<Entity> {
     private static final String MOVES_REGEX = "^\\s*([^\\s])";
     private static final Pattern MOVES_PATTERN = Pattern.compile(MOVES_REGEX);
 
+    private static final String RESULT_REGEX = "^\\s*(1-0|0-1|1/2-1/2|\\*)";
+    private static final Pattern RESULT_PATTERN = Pattern.compile(RESULT_REGEX);
+
     private static final String COMMENT = "{";
     private static final String VARIATION = "(";
 
     @Override
     public int token(Entity entity, String pgn) throws ParseException {
         int pgnLength = pgn.length();
-        pgn = PGNUtils.stripCrlf(pgn);
-        Token<Moves> token = assignToken(pgn);
-        while (token != null) {
-            int index = token.token(entity, pgn);
+        pgn = PGNUtils.stripCrlf(pgn); // todo: starting with ";" comments
+        boolean isResulted = false;
+        Type tokenType = tokenType(pgn);
+        while (tokenType != null) {
+            if (isResulted) {
+                throw new ParseException("parse.pgn.symbols.after.result", pgn);
+            }
+            int index = tokenType.token().token(entity, pgn);
+            if (tokenType.equals(Type.RESULT)) isResulted = true;
             pgn = pgn.substring(index);
-            token = assignToken(pgn);
+            tokenType = tokenType(pgn);
         }
         return pgnLength;
     }
 
-    private Token<Moves> assignToken(String pgn) {
-        Token<Moves> token = null;
-        Matcher moveMatcher = MOVES_PATTERN.matcher(pgn);
-        if (moveMatcher.find()) {
-            switch (moveMatcher.group(1)) {
-                case COMMENT:
-                    token = Type.COMMENT.token();
-                    break;
-                case VARIATION:
-                    token = Type.VARIATION.token();
-                    break;
-                default:
-                    token = Type.MOVE.token();
+    private Type tokenType(String pgn) {
+        Type type = null;
+        if (RESULT_PATTERN.matcher(pgn).find()) {
+            type = Type.RESULT;
+        } else {
+            Matcher moveMatcher = MOVES_PATTERN.matcher(pgn);
+            if (moveMatcher.find()) {
+                switch (moveMatcher.group(1)) {
+                    case COMMENT:
+                        type = Type.COMMENT;
+                        break;
+                    case VARIATION:
+                        type = Type.VARIATION;
+                        break;
+                    default:
+                        type = Type.MOVE;
+                }
             }
         }
-        return token;
+        return type;
     }
 
 }
diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/ResultToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/ResultToken.java
new file mode 100644 (file)
index 0000000..5348619
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2020. Developed by Hedgecode.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hedgecode.chess.pgn.token;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.hedgecode.chess.ParseException;
+import org.hedgecode.chess.pgn.PGNUtils;
+import org.hedgecode.chess.pgn.entity.Moves;
+
+/**
+ * ResultToken
+ *
+ * @author Dmitry Samoshin aka gotty
+ */
+public class ResultToken implements Token<Moves> {
+
+    private static final String WHITE_WIN = "1-0";
+    private static final String BLACK_WIN = "0-1";
+    private static final String DRAW_GAME = "1/2-1/2";
+    private static final String PROGRESS_GAME = "*";
+
+    private static final String RESULT_REGEX = String.format(
+            "^\\s*(%s|%s|%s|%s)",
+            WHITE_WIN, BLACK_WIN, DRAW_GAME,
+            PGNUtils.shield(
+                    PROGRESS_GAME, PROGRESS_GAME.toCharArray()
+            )
+    );
+    private static final Pattern RESULT_PATTERN = Pattern.compile(RESULT_REGEX);
+    private static final int RESULT_GROUP = 1;
+
+    @Override
+    public int token(Moves moves, String pgn) throws ParseException {
+        Matcher matcher = RESULT_PATTERN.matcher(pgn);
+        if (!matcher.find()) {
+            throw new ParseException("parse.pgn.incorrect.result", pgn);
+        } else {
+            String result = matcher.group(RESULT_GROUP);
+            moves.setResult(result);
+        }
+        return matcher.group().length();
+    }
+
+}
index e2550b9..874cf81 100644 (file)
@@ -20,7 +20,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.hedgecode.chess.ParseException;
-import org.hedgecode.chess.pgn.PGNConstants;
+import org.hedgecode.chess.pgn.PGNUtils;
 import org.hedgecode.chess.pgn.entity.Game;
 
 /**
@@ -31,12 +31,13 @@ import org.hedgecode.chess.pgn.entity.Game;
 public class TagToken implements Token<Game> {
 
     private static final String TAG_REGEX = "^\\[([^\"]+)\\s\"([^\"]+)\"\\]$";
-    private static final Pattern TAG_PATTERN = Pattern.compile(TAG_REGEX, Pattern.MULTILINE);
+    private static final Pattern TAG_PATTERN = Pattern.compile(TAG_REGEX);
     private static final int NAME_GROUP = 1, VALUE_GROUP = 2;
 
     @Override
     public int token(Game game, String pgn) throws ParseException {
-        Matcher matcher = TAG_PATTERN.matcher(pgn);
+        String tag = PGNUtils.nextLine(pgn);
+        Matcher matcher = TAG_PATTERN.matcher(tag);
         if (!matcher.find()) {
             throw new ParseException("parse.pgn.incorrect.tag", pgn);
         } else {
@@ -44,7 +45,7 @@ public class TagToken implements Token<Game> {
             String value = matcher.group(VALUE_GROUP);
             game.addTag(name, value);
         }
-        return pgn.indexOf(PGNConstants.PGN_CRLF) + 1;
+        return tag.length();
     }
 
 }
index 9a843a0..be3b34d 100644 (file)
@@ -19,8 +19,11 @@ package org.hedgecode.chess.pgn.token;
 import java.util.regex.Pattern;
 
 import org.hedgecode.chess.ParseException;
+import org.hedgecode.chess.pgn.PGNUtils;
 import org.hedgecode.chess.pgn.entity.Game;
 
+import static org.hedgecode.chess.pgn.PGNConstants.*;
+
 /**
  * Tokenizer
  *
@@ -45,18 +48,22 @@ public class Tokenizer {
         }
     }
 
-    private static final Pattern TAG_PATTERN = Pattern.compile("^\\[[^]]+\\]$", Pattern.MULTILINE);
-    private static final Pattern EMPTY_PATTERN = Pattern.compile("^\\s*$", Pattern.MULTILINE);
-    private static final Pattern MOVES_PATTERN = Pattern.compile("^\\s*([^\\s])", Pattern.MULTILINE);
+    private static final Pattern TAG_PATTERN = Pattern.compile("^\\[[^]]+\\]$");
+    private static final Pattern EMPTY_PATTERN = Pattern.compile("^\\s*$");
+    private static final Pattern MOVES_PATTERN = Pattern.compile("^\\s*[0-9{(;*]+");
 
-    private final String pgn;
+    private final String originalPgn;
 
     private Token<Game> token;
     private String tokenPgn;
 
+    private boolean isTagExists = false;
+    private boolean isEmptyExists = false;
+    private boolean isMovesExists = false;
+
     public Tokenizer(String pgn) {
-        this.pgn = pgn;
-        this.tokenPgn = pgn;
+        this.originalPgn = pgn;
+        this.tokenPgn = pgn.endsWith(PGN_CRLF) ? pgn : pgn.concat(PGN_CRLF);
     }
 
     public boolean hasToken() {
@@ -65,29 +72,43 @@ public class Tokenizer {
     }
 
     public void token(Game game) throws ParseException {
-        if (token == null) {
-            throw new ParseException("parse.pgn.null.token");
-        }
+        assertToken();
         int index = token.token(game, tokenPgn);
         tokenPgn = tokenPgn.substring(index);
         token = null;
     }
 
-    public String sourcePgn() {
-        return pgn;
+    public String originalPgn() {
+        return originalPgn;
     }
 
     private void assignToken() {
         token = null;
         if (!tokenPgn.isEmpty()) {
-            if (TAG_PATTERN.matcher(tokenPgn).find()) {
+            String pgnLine = PGNUtils.nextLine(tokenPgn);
+            if (TAG_PATTERN.matcher(pgnLine).find()) {
+                isTagExists = true;
                 token = Type.TAG.token();
-            } else if (EMPTY_PATTERN.matcher(tokenPgn).find()) {
+            } else if (EMPTY_PATTERN.matcher(pgnLine).find()) {
+                isEmptyExists = true;
                 token = Type.EMPTY.token();
-            } else if (MOVES_PATTERN.matcher(tokenPgn).find()) {
+            } else if (MOVES_PATTERN.matcher(pgnLine).find()) {
+                isMovesExists = true;
                 token = Type.MOVES.token();
             }
         }
     }
 
+    private void assertToken() throws ParseException {
+        if (token == null) {
+            throw new ParseException("parse.pgn.null.token");
+        } else {
+            if (!isTagExists && (isEmptyExists || isMovesExists)) {
+                throw new ParseException("parse.pgn.tag.not.exist");
+            } else if (!isEmptyExists && isMovesExists) {
+                throw new ParseException("parse.pgn.missed.empty");
+            }
+        }
+    }
+
 }
index 7ee717a..cea4ede 100644 (file)
@@ -39,7 +39,7 @@ public class VariationToken implements Token<Moves> {
     public int token(Moves moves, String pgn) throws ParseException {
         int startToken  = pgn.indexOf(OPEN_VARIATION);
         int endToken = endToken(pgn, startToken);
-        Variation variation = new Variation();
+        Variation variation = new Variation(moves);
         Token<Variation> variationToken = new MovesToken<>();
         variationToken.token(
                 variation, pgn.substring(startToken + 1, endToken)
diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/CommonSANToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/CommonSANToken.java
new file mode 100644 (file)
index 0000000..e66b08a
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2018-2020. Developed by Hedgecode.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hedgecode.chess.pgn.token.san;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.hedgecode.chess.ParseException;
+
+/**
+ * CommonSANToken
+ *
+ * @author Dmitry Samoshin aka gotty
+ */
+public class CommonSANToken implements SANToken {
+
+    private static final String COMMON = "[PNBRQK]?[a-h]?[1-8]?[x]?[a-h][1-8]";
+    private static final String CASTLING = "O-O-O|O-O";
+    private static final String PROMOTION = "=[NBRQ]";
+    private static final String CHECKMATE = "[+#]";
+    private static final String EVALUATE = "[!?]{1,2}";
+
+    private static final String[] MOVE_EVALUATE = { "!", "?", "!!", "!?", "?!", "??" };
+
+    private static final String ENDS_WITH_FORMAT = "(%s)$";
+
+    private static final Pattern COMMON_PATTERN = Pattern.compile(COMMON);
+    private static final Pattern CASTLING_PATTERN = Pattern.compile(CASTLING);
+
+    @Override
+    public int token(String san) throws ParseException {
+        String tokenSan = san;
+        int count = isEvaluate(tokenSan);
+        if (count > 0) {
+            tokenSan = cutEnd(tokenSan, count);
+        }
+        count = isCheckmate(tokenSan);
+        if (count > 0) {
+            tokenSan = cutEnd(tokenSan, count);
+        }
+        if (!isCastling(tokenSan)) {
+            count = isPromotion(tokenSan);
+            if (count > 0) {
+                tokenSan = cutEnd(tokenSan, count);
+            }
+            if (!isCommon(tokenSan)) {
+                throw new ParseException("parse.pgn.incorrect.move", san);
+            }
+        }
+        return san.length();
+    }
+
+    protected boolean isCommon(String san) {
+        return COMMON_PATTERN.matcher(san).matches();
+    }
+
+    protected boolean isCastling(String san) {
+        return CASTLING_PATTERN.matcher(san).matches();
+    }
+
+    protected int isPromotion(String san) {
+        return endsWith(san, PROMOTION);
+    }
+
+    protected int isCheckmate(String san) {
+        return endsWith(san, CHECKMATE);
+    }
+
+    protected int isEvaluate(String san) {
+        return endsWith(san, EVALUATE);
+    }
+
+    private int endsWith(String san, String regex) {
+        Matcher endsWithMatcher = Pattern.compile(
+                String.format(ENDS_WITH_FORMAT, regex)
+        ).matcher(san);
+        return endsWithMatcher.find()
+                ? endsWithMatcher.group(1).length()
+                : 0;
+    }
+
+    private String cutEnd(String san, int count) {
+        return san.substring(0, san.length() - count);
+    }
+
+}
diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/LegalSANToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/LegalSANToken.java
new file mode 100644 (file)
index 0000000..8a4c7e5
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2018-2020. Developed by Hedgecode.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hedgecode.chess.pgn.token.san;
+
+import org.hedgecode.chess.ParseException;
+import org.hedgecode.chess.pgn.entity.Moves;
+
+/**
+ * LegalSANToken
+ *
+ * @author Dmitry Samoshin aka gotty
+ */
+public class LegalSANToken extends CommonSANToken {
+
+    private final Moves moves;
+
+    public LegalSANToken(Moves moves) {
+        this.moves = moves;
+    }
+
+    @Override
+    public int token(String san) throws ParseException {
+        return super.token(san);
+    }
+
+}
diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/SANToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/SANToken.java
new file mode 100644 (file)
index 0000000..f43efe1
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2018-2020. Developed by Hedgecode.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hedgecode.chess.pgn.token.san;
+
+import org.hedgecode.chess.ParseException;
+import org.hedgecode.chess.pgn.token.Token;
+
+/**
+ * SANToken
+ *
+ * @author Dmitry Samoshin aka gotty
+ */
+public interface SANToken extends Token<Void> {
+
+    int token(String san) throws ParseException;
+
+    @Override
+    default int token(Void voidType, String san) throws ParseException {
+        return token(san);
+    }
+
+}
index 67f3df1..03550db 100644 (file)
@@ -21,11 +21,17 @@ parse.fen.invalid.string=Invalid input
 parse.fen.incorrect.board=Incorrect board
 
 parse.pgn.null.token=Unable to define token for PGN
+parse.pgn.tag.not.exist=No PGN tags found
 parse.pgn.incorrect.tag=Incorrect PGN tag
-parse.pgn.incorrect.empty=Missed blank line in PGN
+parse.pgn.missed.empty=Missed PGN empty line
+parse.pgn.incorrect.empty=Incorrect PGN empty line
 parse.pgn.incorrect.move=Incorrect PGN move
 parse.pgn.incorrect.comment=Incorrect PGN comment
 parse.pgn.incorrect.variation=Incorrect PGN variation
+parse.pgn.incorrect.result=Incorrect PGN result
+parse.pgn.symbols.after.result=Incorrect symbols after PGN result
+parse.pgn.result.not.match=Result in PGN tags and moves does not match
+parse.pgn.variation.result=Variations cannot contain PGN result
 
 parse.tcd.index.out.of.bounds=Array Index Out Of Bounds
 parse.tcd.invalid.bytes=Invalid input bytes
index ce7ffa7..2b913d5 100644 (file)
@@ -20,11 +20,17 @@ parse.fen.invalid.string=
 parse.fen.incorrect.board=
 
 parse.pgn.null.token=
+parse.pgn.tag.not.exist=
 parse.pgn.incorrect.tag=
+parse.pgn.missed.empty=
 parse.pgn.incorrect.empty=
 parse.pgn.incorrect.move=
 parse.pgn.incorrect.comment=
 parse.pgn.incorrect.variation=
+parse.pgn.incorrect.result=
+parse.pgn.symbols.after.result=
+parse.pgn.result.not.match=
+parse.pgn.variation.result=
 
 parse.tcd.index.out.of.bounds=
 parse.tcd.invalid.bytes=