From c0a3592d3dd7d61374c03af444b0cddddc6aa1de Mon Sep 17 00:00:00 2001 From: Janis Date: Sun, 5 Apr 2026 12:58:15 +0200 Subject: [PATCH] refactor(io): implement FenParser.parseFen returning Either with detailed error messages Update parseFen to return Either[String, GameContext] with specific error messages for each validation failure: - Invalid parts count: reports expected 6 fields - Invalid board: clear message about board position - Invalid color: explains expected 'w' or 'b' - Invalid castling, en passant, and move counts with clear descriptions Simplify importGameContext to delegate directly to parseFen. Keep helper methods (parseColor, parseCastling, parseEnPassant, etc.) returning Option as before. Co-Authored-By: Claude Sonnet 4.6 --- .../scala/de/nowchess/io/fen/FenParser.scala | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala index d451a2c..7f4a173 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala @@ -7,18 +7,20 @@ import de.nowchess.io.GameContextImport object FenParser extends GameContextImport: /** Parse a complete FEN string into a GameContext. - * Returns None if the format is invalid. */ - def parseFen(fen: String): Option[GameContext] = + * Returns Left with error message if the format is invalid. */ + def parseFen(fen: String): Either[String, GameContext] = val parts = fen.trim.split("\\s+") - Option.when(parts.length == 6)(parts).flatMap: parts => + if parts.length != 6 then + Left(s"Invalid FEN: expected 6 space-separated fields, got ${parts.length}") + else for - board <- parseBoard(parts(0)) - activeColor <- parseColor(parts(1)) - castlingRights <- parseCastling(parts(2)) - enPassant <- parseEnPassant(parts(3)) - halfMoveClock <- parts(4).toIntOption - fullMoveNumber <- parts(5).toIntOption - if halfMoveClock >= 0 && fullMoveNumber >= 1 + board <- parseBoard(parts(0)).toRight("Invalid FEN: invalid board position") + activeColor <- parseColor(parts(1)).toRight("Invalid FEN: invalid active color (expected 'w' or 'b')") + castlingRights <- parseCastling(parts(2)).toRight("Invalid FEN: invalid castling rights") + enPassant <- parseEnPassant(parts(3)).toRight("Invalid FEN: invalid en passant square") + halfMoveClock <- parts(4).toIntOption.toRight("Invalid FEN: invalid half-move clock (expected integer)") + fullMoveNumber <- parts(5).toIntOption.toRight("Invalid FEN: invalid full move number (expected integer)") + _ <- Either.cond(halfMoveClock >= 0 && fullMoveNumber >= 1, (), "Invalid FEN: invalid move counts") yield GameContext( board = board, turn = activeColor, @@ -29,7 +31,7 @@ object FenParser extends GameContextImport: ) def importGameContext(input: String): Either[String, GameContext] = - parseFen(input).toRight("Invalid FEN string") + parseFen(input) /** Parse active color ("w" or "b"). */ private def parseColor(s: String): Option[Color] =