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)";
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import static org.hedgecode.chess.pgn.PGNConstants.*;
+
/**
* 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(
public static boolean isPgn(String source) {
return match(
source,
- PGNConstants.PGN_DETECT_REGEX
+ PGN_DETECT_REGEX
) != null;
}
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() {
import java.util.List;
import java.util.Map;
+import org.hedgecode.chess.ParseException;
+
/**
* DetailGame
*
}
@Override
+ public Moves parent() {
+ return null;
+ }
+
+ @Override
public DetailMove nullMove() {
return NULL_MOVE;
}
}
@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);
}
import java.util.ArrayList;
import java.util.List;
+import org.hedgecode.chess.pgn.nag.Glyph;
+
/**
* DetailMove
*
private int ply;
private String move;
+ private List<Glyph> glyphs = new ArrayList<>();
private List<String> comments = new ArrayList<>();
private List<Variation> variations = new ArrayList<>();
return move;
}
+ public void addGlyph(Glyph glyph) {
+ glyphs.add(glyph);
+ }
+
public void addComment(String comment) {
comments.add(comment);
}
variations.add(variation);
}
+ public List<Glyph> glyphs() {
+ return glyphs;
+ }
+
public List<String> comments() {
return comments;
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
import java.util.List;
+import org.hedgecode.chess.ParseException;
+
/**
* Moves
*
List<DetailMove> getMoves();
+ Moves parent();
+
DetailMove nullMove();
DetailMove currentMove();
+ void setResult(String result) throws ParseException;
+
+ String result();
+
}
import java.util.ArrayList;
import java.util.List;
+import org.hedgecode.chess.ParseException;
+
/**
* Variation
*
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
}
@Override
+ public Moves parent() {
+ return parentMoves;
+ }
+
+ @Override
public DetailMove nullMove() {
return NULL_MOVE;
}
return currentMove;
}
+ @Override
+ public void setResult(String result) throws ParseException {
+ throw new ParseException("parse.pgn.variation.result");
+ }
+
+ @Override
+ public String result() {
+ return null;
+ }
+
}
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.*;
private String formatAddition(DetailMove move) {
StringBuilder sb = new StringBuilder();
sb.append(
+ formatNag(move)
+ ).append(
formatComment(move)
).append(
formatVariation(move)
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()) {
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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;
/**
*/
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();
}
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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
*/
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();
}
}
MOVE ( new MoveToken() ),
COMMENT ( new CommentToken() ),
- VARIATION ( new VariationToken() );
+ VARIATION ( new VariationToken() ),
+ RESULT ( new ResultToken() );
private Token<Moves> 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<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;
}
}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
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;
/**
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 {
String value = matcher.group(VALUE_GROUP);
game.addTag(name, value);
}
- return pgn.indexOf(PGNConstants.PGN_CRLF) + 1;
+ return tag.length();
}
}
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
*
}
}
- 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() {
}
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");
+ }
+ }
+ }
+
}
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)
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
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
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=