From 0834aacdb3f82d7fe2f0762f3bc823c5f9e3eb47 Mon Sep 17 00:00:00 2001 From: gotty Date: Mon, 27 Jan 2020 03:39:41 +0300 Subject: [PATCH] [LIB-9] Add SAN, NAG, move number and result PGN tokens --- .../java/org/hedgecode/chess/pgn/PGNConstants.java | 1 + .../java/org/hedgecode/chess/pgn/PGNUtils.java | 18 +++- .../org/hedgecode/chess/pgn/entity/DetailGame.java | 22 +++++ .../org/hedgecode/chess/pgn/entity/DetailMove.java | 11 +++ .../org/hedgecode/chess/pgn/entity/MoveNumber.java | 50 +++++++++++ .../java/org/hedgecode/chess/pgn/entity/Moves.java | 8 ++ .../org/hedgecode/chess/pgn/entity/Variation.java | 24 ++++- .../chess/pgn/format/ExportMovesFormat.java | 13 +++ .../java/org/hedgecode/chess/pgn/nag/Glyph.java | 76 ++++++++++++++++ .../org/hedgecode/chess/pgn/token/EmptyToken.java | 14 +-- .../hedgecode/chess/pgn/token/MoveNumberToken.java | 51 +++++++++++ .../org/hedgecode/chess/pgn/token/MoveToken.java | 44 ++++++--- .../org/hedgecode/chess/pgn/token/MovesToken.java | 53 ++++++----- .../org/hedgecode/chess/pgn/token/ResultToken.java | 60 +++++++++++++ .../org/hedgecode/chess/pgn/token/TagToken.java | 9 +- .../org/hedgecode/chess/pgn/token/Tokenizer.java | 49 +++++++--- .../hedgecode/chess/pgn/token/VariationToken.java | 2 +- .../chess/pgn/token/san/CommonSANToken.java | 100 +++++++++++++++++++++ .../chess/pgn/token/san/LegalSANToken.java | 40 +++++++++ .../hedgecode/chess/pgn/token/san/SANToken.java | 36 ++++++++ .../org/hedgecode/chess/LocalStrings.properties | 8 +- .../org/hedgecode/chess/LocalStrings_ru.properties | 6 ++ 22 files changed, 625 insertions(+), 70 deletions(-) create mode 100644 chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/MoveNumber.java create mode 100644 chesshog-format/src/main/java/org/hedgecode/chess/pgn/nag/Glyph.java create mode 100644 chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveNumberToken.java create mode 100644 chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/ResultToken.java create mode 100644 chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/CommonSANToken.java create mode 100644 chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/LegalSANToken.java create mode 100644 chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/SANToken.java diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNConstants.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNConstants.java index 45b2408..c0e3660 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNConstants.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNConstants.java @@ -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)"; diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNUtils.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNUtils.java index 822c31f..884a388 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNUtils.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/PGNUtils.java @@ -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(CRLF, SPACE); + return pgn.replaceAll(CRLF_REGEX, PGN_SPACE); } private PGNUtils() { diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailGame.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailGame.java index 7486dc0..c80eb72 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailGame.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailGame.java @@ -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); } diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailMove.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailMove.java index 2b37204..4463a77 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailMove.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/DetailMove.java @@ -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 glyphs = new ArrayList<>(); private List comments = new ArrayList<>(); private List 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 glyphs() { + return glyphs; + } + public List 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 index 0000000..0ba4571 --- /dev/null +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/MoveNumber.java @@ -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; + } + +} diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Moves.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Moves.java index c751132..36704a3 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Moves.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Moves.java @@ -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 getMoves(); + Moves parent(); + DetailMove nullMove(); DetailMove currentMove(); + void setResult(String result) throws ParseException; + + String result(); + } diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Variation.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Variation.java index 3c290e8..c7a8c9a 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Variation.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/entity/Variation.java @@ -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 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; + } + } diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/format/ExportMovesFormat.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/format/ExportMovesFormat.java index 4de3b41..404f8da 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/format/ExportMovesFormat.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/format/ExportMovesFormat.java @@ -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 index 0000000..b343300 --- /dev/null +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/nag/Glyph.java @@ -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; + } + +} diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/EmptyToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/EmptyToken.java index 2fcbc86..082e3d7 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/EmptyToken.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/EmptyToken.java @@ -16,11 +16,8 @@ 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 { - 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 index 0000000..f958ba2 --- /dev/null +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveNumberToken.java @@ -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 { + + 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; + } + +} diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveToken.java index de59a2c..ea89dfc 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveToken.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MoveToken.java @@ -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 { - 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 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(); } } diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MovesToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MovesToken.java index e766190..78de96c 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MovesToken.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/MovesToken.java @@ -34,7 +34,8 @@ public class MovesToken implements Token { MOVE ( new MoveToken() ), COMMENT ( new CommentToken() ), - VARIATION ( new VariationToken() ); + VARIATION ( new VariationToken() ), + RESULT ( new ResultToken() ); private Token token; @@ -50,38 +51,50 @@ public class MovesToken implements Token { 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 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 assignToken(String pgn) { - Token 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 index 0000000..5348619 --- /dev/null +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/ResultToken.java @@ -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 { + + 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(); + } + +} diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/TagToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/TagToken.java index e2550b9..874cf81 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/TagToken.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/TagToken.java @@ -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 { 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 { String value = matcher.group(VALUE_GROUP); game.addTag(name, value); } - return pgn.indexOf(PGNConstants.PGN_CRLF) + 1; + return tag.length(); } } diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/Tokenizer.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/Tokenizer.java index 9a843a0..be3b34d 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/Tokenizer.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/Tokenizer.java @@ -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 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"); + } + } + } + } diff --git a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/VariationToken.java b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/VariationToken.java index 7ee717a..cea4ede 100644 --- a/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/VariationToken.java +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/VariationToken.java @@ -39,7 +39,7 @@ public class VariationToken implements Token { 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 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 index 0000000..e66b08a --- /dev/null +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/CommonSANToken.java @@ -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 index 0000000..8a4c7e5 --- /dev/null +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/LegalSANToken.java @@ -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 index 0000000..f43efe1 --- /dev/null +++ b/chesshog-format/src/main/java/org/hedgecode/chess/pgn/token/san/SANToken.java @@ -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 { + + int token(String san) throws ParseException; + + @Override + default int token(Void voidType, String san) throws ParseException { + return token(san); + } + +} diff --git a/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings.properties b/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings.properties index 67f3df1..03550db 100644 --- a/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings.properties +++ b/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings.properties @@ -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 diff --git a/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings_ru.properties b/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings_ru.properties index ce7ffa7..2b913d5 100644 --- a/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings_ru.properties +++ b/chesshog-format/src/main/resources/org/hedgecode/chess/LocalStrings_ru.properties @@ -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= -- 2.10.0