feen 5.0.0.beta2 → 5.0.0.beta4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module PiecesInHand
6
+ # Error messages for validation
7
+ Errors = {
8
+ invalid_type: "Pieces in hand must be a string, got %s",
9
+ empty_string: "Pieces in hand string cannot be empty",
10
+ invalid_format: "Invalid pieces in hand format: %s",
11
+ sorting_error: "Pieces in hand must be in ASCII lexicographic order"
12
+ }.freeze
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module PiecesInHand
6
+ # Character used to represent no pieces in hand
7
+ NoPieces = "-"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module PiecesInHand
6
+ # Regex to extract piece counts from pieces in hand string
7
+ # Matches either:
8
+ # - A single piece character with no count (e.g., "P")
9
+ # - A count followed by a piece character (e.g., "5P")
10
+ PieceCountPattern = /(?:([2-9]|\d{2,}))?([A-Za-z])/
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module PiecesInHand
6
+ # Valid pattern for pieces in hand based on BNF specification.
7
+ #
8
+ # The FEEN format specifies these rules for numeric prefixes:
9
+ # - Cannot start with "0"
10
+ # - Cannot be exactly "1" (use the letter without prefix instead)
11
+ # - Can be 2-9 or any number with 2+ digits (10, 11, etc.)
12
+ #
13
+ # The pattern matches either:
14
+ # - A single digit from 2-9
15
+ # - OR any number with two or more digits (10, 11, 27, 103, etc.)
16
+ #
17
+ # @return [Regexp] Regular expression for validating pieces in hand format
18
+ ValidFormatPattern = /\A
19
+ (?:
20
+ -| # No pieces in hand
21
+ (?: # Or sequence of pieces
22
+ (?:(?:[2-9]|\d{2,})?[A-Z])* # Uppercase pieces (optional)
23
+ (?:(?:[2-9]|\d{2,})?[a-z])* # Lowercase pieces (optional)
24
+ )
25
+ )
26
+ \z/x
27
+ end
28
+ end
29
+ end
@@ -1,74 +1,119 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative File.join("pieces_in_hand", "errors")
4
+ require_relative File.join("pieces_in_hand", "no_pieces")
5
+ require_relative File.join("pieces_in_hand", "piece_count_pattern")
6
+ require_relative File.join("pieces_in_hand", "valid_format_pattern")
7
+
3
8
  module Feen
4
9
  module Parser
5
- # Handles parsing of the pieces in hand section of a FEEN string
10
+ # Handles parsing of the pieces in hand section of a FEEN string.
11
+ # Pieces in hand represent pieces available for dropping onto the board.
6
12
  module PiecesInHand
7
- NO_PIECES = "-"
8
- ERRORS = {
9
- invalid_type: "Pieces in hand must be a string, got %s",
10
- empty_string: "Pieces in hand string cannot be empty",
11
- invalid_chars: "Invalid characters in pieces in hand: %s",
12
- invalid_identifier: "Invalid piece identifier at position %d"
13
- }.freeze
14
-
15
- # Parses the pieces in hand section of a FEEN string
13
+ # Parses the pieces in hand section of a FEEN string.
16
14
  #
17
15
  # @param pieces_in_hand_str [String] FEEN pieces in hand string
18
- # @return [Array<Hash>] Array of pieces in hand
16
+ # @return [Array<String>] Array of single-character piece identifiers in the
17
+ # format specified in the FEEN string (no prefixes or suffixes), expanded
18
+ # based on their counts and sorted in ASCII lexicographic order.
19
+ # Empty array if no pieces are in hand.
19
20
  # @raise [ArgumentError] If the input string is invalid
21
+ #
22
+ # @example Parse no pieces in hand
23
+ # PiecesInHand.parse("-")
24
+ # # => []
25
+ #
26
+ # @example Parse multiple pieces in hand
27
+ # PiecesInHand.parse("BN2Pb")
28
+ # # => ["B", "N", "P", "P", "b"]
29
+ #
30
+ # @example Parse pieces with counts
31
+ # PiecesInHand.parse("N5P2b")
32
+ # # => ["N", "P", "P", "P", "P", "P", "b", "b"]
20
33
  def self.parse(pieces_in_hand_str)
21
- validate_pieces_in_hand_string(pieces_in_hand_str)
22
-
23
- # Handle the special case of no pieces in hand
24
- return [] if pieces_in_hand_str == NO_PIECES
34
+ # Validate input
35
+ validate_input_type(pieces_in_hand_str)
36
+ validate_format(pieces_in_hand_str)
25
37
 
26
- pieces = []
27
- i = 0
38
+ # Handle the no-pieces case early
39
+ return [] if pieces_in_hand_str == NoPieces
28
40
 
29
- while i < pieces_in_hand_str.length
30
- # Vérifier que le caractère est une lettre
31
- raise ArgumentError, format(ERRORS[:invalid_identifier], i) unless pieces_in_hand_str[i].match?(/[a-zA-Z]/)
41
+ # Extract pieces with their counts and validate the order
42
+ pieces_with_counts = extract_pieces_with_counts(pieces_in_hand_str)
43
+ validate_lexicographic_order(pieces_with_counts)
32
44
 
33
- pieces << { id: pieces_in_hand_str[i] }
34
- i += 1
45
+ # Expand the pieces into an array and maintain lexicographic ordering
46
+ expand_pieces(pieces_with_counts)
47
+ end
35
48
 
36
- end
49
+ # Validates that the input is a non-empty string.
50
+ #
51
+ # @param str [String] Input string to validate
52
+ # @raise [ArgumentError] If input is not a string or is empty
53
+ # @return [void]
54
+ private_class_method def self.validate_input_type(str)
55
+ raise ::ArgumentError, format(Errors[:invalid_type], str.class) unless str.is_a?(::String)
56
+ raise ::ArgumentError, Errors[:empty_string] if str.empty?
57
+ end
37
58
 
38
- # Vérifier que les pièces sont triées par ordre lexicographique
39
- raise ArgumentError, "Pieces in hand must be in ASCII lexicographic order" unless pieces_sorted?(pieces)
59
+ # Validates that the input string matches the expected format according to FEEN specification.
60
+ #
61
+ # @param str [String] Input string to validate
62
+ # @raise [ArgumentError] If format is invalid
63
+ # @return [void]
64
+ private_class_method def self.validate_format(str)
65
+ return if str == NoPieces || str.match?(ValidFormatPattern)
40
66
 
41
- pieces
67
+ raise ::ArgumentError, format(Errors[:invalid_format], str)
42
68
  end
43
69
 
44
- # Validates the pieces in hand string for syntax
70
+ # Extracts pieces with their counts from the FEEN string.
45
71
  #
46
72
  # @param str [String] FEEN pieces in hand string
47
- # @raise [ArgumentError] If the string is invalid
48
- # @return [void]
49
- def self.validate_pieces_in_hand_string(str)
50
- raise ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(String)
73
+ # @return [Array<Hash>] Array of hashes with :piece and :count keys
74
+ private_class_method def self.extract_pieces_with_counts(str)
75
+ result = []
76
+ position = 0
77
+
78
+ while position < str.length
79
+ match = str[position..].match(PieceCountPattern)
80
+ break unless match
51
81
 
52
- raise ArgumentError, ERRORS[:empty_string] if str.empty?
82
+ count_str, piece = match.captures
83
+ count = count_str ? count_str.to_i : 1
53
84
 
54
- # Check for the special case of no pieces in hand
55
- return if str == NO_PIECES
85
+ # Add to our result with piece type and count
86
+ result << { piece: piece, count: count }
56
87
 
57
- # Check for valid characters (only letters)
58
- valid_chars = /\A[a-zA-Z]+\z/
59
- return if str.match?(valid_chars)
88
+ # Move position forward
89
+ position += match[0].length
90
+ end
60
91
 
61
- invalid_chars = str.scan(/[^a-zA-Z]/).uniq.join(", ")
62
- raise ArgumentError, format(ERRORS[:invalid_chars], invalid_chars)
92
+ result
63
93
  end
64
94
 
65
- # Checks if pieces are sorted in ASCII lexicographic order
95
+ # Validates that pieces are in lexicographic ASCII order.
66
96
  #
67
- # @param pieces [Array<Hash>] Array of piece hashes
68
- # @return [Boolean] True if pieces are sorted
69
- def self.pieces_sorted?(pieces)
70
- piece_ids = pieces.map { |piece| piece[:id] }
71
- piece_ids == piece_ids.sort
97
+ # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
98
+ # @raise [ArgumentError] If pieces are not in lexicographic order
99
+ # @return [void]
100
+ private_class_method def self.validate_lexicographic_order(pieces_with_counts)
101
+ pieces = pieces_with_counts.map { |item| item[:piece] }
102
+
103
+ # Verify the array is sorted lexicographically
104
+ return if pieces == pieces.sort
105
+
106
+ raise ::ArgumentError, Errors[:sorting_error]
107
+ end
108
+
109
+ # Expands the pieces based on their counts into an array.
110
+ #
111
+ # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
112
+ # @return [Array<String>] Array of expanded pieces
113
+ private_class_method def self.expand_pieces(pieces_with_counts)
114
+ pieces_with_counts.flat_map do |item|
115
+ Array.new(item[:count], item[:piece])
116
+ end
72
117
  end
73
118
  end
74
119
  end
data/lib/feen/parser.rb CHANGED
@@ -5,48 +5,89 @@ require_relative File.join("parser", "piece_placement")
5
5
  require_relative File.join("parser", "pieces_in_hand")
6
6
 
7
7
  module Feen
8
- # Module responsible for parsing FEEN notation strings into internal data structures
8
+ # Module responsible for parsing FEEN notation strings into internal data structures.
9
+ # FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic
10
+ # textual format for representing static board positions in two-player piece-placement games.
9
11
  module Parser
12
+ # Regular expression pattern for matching a valid FEEN string structure
13
+ # Captures exactly three non-space components separated by single spaces
14
+ FEEN_PATTERN = /\A([^\s]+)\s([^\s]+)\s([^\s]+)\z/
15
+
16
+ # Error message for invalid FEEN format
17
+ INVALID_FORMAT_ERROR = "Invalid FEEN format: expected exactly 3 fields separated by single spaces"
18
+
10
19
  # Parses a complete FEEN string into a structured representation
11
20
  #
12
21
  # @param feen_string [String] Complete FEEN notation string
13
- # @return [Hash] Hash containing the parsed position data
22
+ # @return [Hash] Hash containing the parsed position data with the following keys:
23
+ # - :piece_placement [Array] - Hierarchical array structure representing the board
24
+ # - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
25
+ # - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
14
26
  # @raise [ArgumentError] If the FEEN string is invalid
27
+ #
28
+ # @example Parsing a standard chess initial position
29
+ # feen = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
30
+ # result = Feen::Parser.parse(feen)
31
+ # # => {
32
+ # # piece_placement: [
33
+ # # ["r", "n", "b", "q", "k=", "b", "n", "r"],
34
+ # # ["p", "p", "p", "p", "p", "p", "p", "p"],
35
+ # # ["", "", "", "", "", "", "", ""],
36
+ # # ["", "", "", "", "", "", "", ""],
37
+ # # ["", "", "", "", "", "", "", ""],
38
+ # # ["", "", "", "", "", "", "", ""],
39
+ # # ["P", "P", "P", "P", "P", "P", "P", "P"],
40
+ # # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
41
+ # # ],
42
+ # # pieces_in_hand: [],
43
+ # # games_turn: ["CHESS", "chess"]
44
+ # # }
15
45
  def self.parse(feen_string)
16
- validate_feen_string(feen_string)
46
+ feen_string = String(feen_string)
17
47
 
18
- # Split the FEEN string into its three fields
19
- fields = feen_string.strip.split(/\s+/)
48
+ # Match the FEEN string against the expected pattern
49
+ match = FEEN_PATTERN.match(feen_string)
20
50
 
21
- raise ArgumentError, "Invalid FEEN format: expected 3 fields, got #{fields.size}" unless fields.size == 3
51
+ # Raise an error if the format doesn't match the expected pattern
52
+ raise ::ArgumentError, INVALID_FORMAT_ERROR unless match
53
+
54
+ # Capture the three distinct parts
55
+ piece_placement_string, pieces_in_hand_string, games_turn_string = match.captures
22
56
 
23
57
  # Parse each field using the appropriate submodule
24
- piece_placement = PiecePlacement.parse(fields[0])
25
- games_turn = GamesTurn.parse(fields[1])
26
- pieces_in_hand = PiecesInHand.parse(fields[2])
58
+ piece_placement = PiecePlacement.parse(piece_placement_string)
59
+ pieces_in_hand = PiecesInHand.parse(pieces_in_hand_string)
60
+ games_turn = GamesTurn.parse(games_turn_string)
27
61
 
28
- # Return a structured representation of the position
62
+ # Create a structured representation of the position
29
63
  {
30
- piece_placement: piece_placement,
31
- games_turn: games_turn,
32
- pieces_in_hand: pieces_in_hand
64
+ piece_placement:,
65
+ pieces_in_hand:,
66
+ games_turn:
33
67
  }
34
68
  end
35
69
 
36
- # Validates the FEEN string for basic format
70
+ # Safely parses a complete FEEN string into a structured representation without raising exceptions
37
71
  #
38
- # @param feen_string [String] FEEN string to validate
39
- # @raise [ArgumentError] If the FEEN string is fundamentally invalid
40
- # @return [void]
41
- def self.validate_feen_string(feen_string)
42
- raise ArgumentError, "FEEN must be a string, got #{feen_string.class}" unless feen_string.is_a?(String)
43
-
44
- raise ArgumentError, "FEEN string cannot be empty" if feen_string.empty?
45
-
46
- # Check for at least two spaces (three fields)
47
- return unless feen_string.count(" ") < 2
48
-
49
- raise ArgumentError, "Invalid FEEN format: must contain at least two spaces separating three fields"
72
+ # This method works like `parse` but returns nil instead of raising an exception
73
+ # if the FEEN string is invalid.
74
+ #
75
+ # @param feen_string [String] Complete FEEN notation string
76
+ # @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
77
+ #
78
+ # @example Parsing a valid FEEN string
79
+ # feen = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
80
+ # result = Feen::Parser.safe_parse(feen)
81
+ # # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
82
+ #
83
+ # @example Parsing an invalid FEEN string
84
+ # feen = "invalid feen string"
85
+ # result = Feen::Parser.safe_parse(feen)
86
+ # # => nil
87
+ def self.safe_parse(feen_string)
88
+ parse(feen_string)
89
+ rescue ::ArgumentError
90
+ nil
50
91
  end
51
92
  end
52
93
  end
data/lib/feen.rb CHANGED
@@ -1,110 +1,129 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("feen", "converter")
4
3
  require_relative File.join("feen", "dumper")
5
4
  require_relative File.join("feen", "parser")
6
5
 
7
6
  # This module provides a Ruby interface for data serialization and
8
7
  # deserialization in FEEN format.
9
8
  #
9
+ # FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and
10
+ # rule-agnostic textual format for representing static board positions
11
+ # in two-player piece-placement games.
12
+ #
10
13
  # @see https://sashite.dev/documents/feen/1.0.0/
11
14
  module Feen
12
- # Dumps position params into a FEEN string.
15
+ # Dumps position components into a FEEN string.
13
16
  #
14
- # @param position [Hash] Hash containing the position data
15
- # @option position [Array] :piece_placement Board position data
16
- # @option position [Hash] :games_turn Games and turn data
17
- # @option position [Array<Hash>] :pieces_in_hand Pieces in hand data
17
+ # @param piece_placement [Array] Board position data structure representing the spatial
18
+ # distribution of pieces across the board
19
+ # @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board
20
+ # @param games_turn [Array<String>] A two-element array where the first element is the
21
+ # active player's variant and the second is the inactive player's variant
18
22
  # @return [String] FEEN notation string
19
- # @raise [ArgumentError] If the position data is invalid
23
+ # @raise [ArgumentError] If any parameter is invalid
20
24
  # @example
21
- # position = {
22
- # piece_placement: [[{id: 'r'}, {id: 'n'}, {id: 'b'}, {id: 'q'}, {id: 'k'}, {id: 'b'}, {id: 'n'}, {id: 'r'}],
23
- # [{id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}],
24
- # [nil, nil, nil, nil, nil, nil, nil, nil],
25
- # [nil, nil, nil, nil, nil, nil, nil, nil],
26
- # [nil, nil, nil, nil, nil, nil, nil, nil],
27
- # [nil, nil, nil, nil, nil, nil, nil, nil],
28
- # [{id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}],
29
- # [{id: 'R'}, {id: 'N'}, {id: 'B'}, {id: 'Q'}, {id: 'K'}, {id: 'B'}, {id: 'N'}, {id: 'R'}]],
30
- # games_turn: {
31
- # active_player: 'CHESS',
32
- # inactive_player: 'chess',
33
- # uppercase_game: 'CHESS',
34
- # lowercase_game: 'chess'
35
- # },
36
- # pieces_in_hand: []
37
- # }
38
- # Feen.dump(position) # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -"
39
- def self.dump(position)
40
- Dumper.dump(position)
25
+ # piece_placement = [
26
+ # ["r", "n", "b", "q", "k=", "b", "n", "r"],
27
+ # ["p", "p", "p", "p", "p", "p", "p", "p"],
28
+ # ["", "", "", "", "", "", "", ""],
29
+ # ["", "", "", "", "", "", "", ""],
30
+ # ["", "", "", "", "", "", "", ""],
31
+ # ["", "", "", "", "", "", "", ""],
32
+ # ["P", "P", "P", "P", "P", "P", "P", "P"],
33
+ # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
34
+ # ]
35
+ # Feen.dump(
36
+ # piece_placement: piece_placement,
37
+ # pieces_in_hand: [],
38
+ # games_turn: ["CHESS", "chess"]
39
+ # )
40
+ # # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
41
+ def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
42
+ Dumper.dump(piece_placement:, pieces_in_hand:, games_turn:)
41
43
  end
42
44
 
43
- # Parses a FEEN string into position params.
45
+ # Parses a FEEN string into position components.
44
46
  #
45
- # @param feen_string [String] FEEN notation string
46
- # @return [Hash] Hash containing the parsed position data
47
+ # @param feen_string [String] Complete FEEN notation string
48
+ # @return [Hash] Hash containing the parsed position data with the following keys:
49
+ # - :piece_placement [Array] - Hierarchical array structure representing the board
50
+ # - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
51
+ # - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
47
52
  # @raise [ArgumentError] If the FEEN string is invalid
48
53
  # @example
49
- # feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -"
54
+ # feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
50
55
  # Feen.parse(feen_string)
51
56
  # # => {
52
- # # piece_placement: [[{id: 'r'}, {id: 'n'}, {id: 'b'}, {id: 'q'}, {id: 'k', suffix: '='}, {id: 'b'}, {id: 'n'}, {id: 'r'}],
53
- # # [{id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}, {id: 'p'}],
54
- # # [nil, nil, nil, nil, nil, nil, nil, nil],
55
- # # [nil, nil, nil, nil, nil, nil, nil, nil],
56
- # # [nil, nil, nil, nil, nil, nil, nil, nil],
57
- # # [nil, nil, nil, nil, nil, nil, nil, nil],
58
- # # [{id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}, {id: 'P'}],
59
- # # [{id: 'R'}, {id: 'N'}, {id: 'B'}, {id: 'Q'}, {id: 'K', suffix: '='}, {id: 'B'}, {id: 'N'}, {id: 'R'}]],
60
- # # games_turn: {
61
- # # active_player: 'CHESS',
62
- # # inactive_player: 'chess',
63
- # # uppercase_game: 'CHESS',
64
- # # lowercase_game: 'chess',
65
- # # active_player_casing: :uppercase
66
- # # },
67
- # # pieces_in_hand: []
57
+ # # piece_placement: [
58
+ # # ["r", "n", "b", "q", "k=", "b", "n", "r"],
59
+ # # ["p", "p", "p", "p", "p", "p", "p", "p"],
60
+ # # ["", "", "", "", "", "", "", ""],
61
+ # # ["", "", "", "", "", "", "", ""],
62
+ # # ["", "", "", "", "", "", "", ""],
63
+ # # ["", "", "", "", "", "", "", ""],
64
+ # # ["P", "P", "P", "P", "P", "P", "P", "P"],
65
+ # # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
66
+ # # ],
67
+ # # pieces_in_hand: [],
68
+ # # games_turn: ["CHESS", "chess"]
68
69
  # # }
69
70
  def self.parse(feen_string)
70
71
  Parser.parse(feen_string)
71
72
  end
72
73
 
73
- # Validates if the given string is a valid FEEN string
74
+ # Safely parses a FEEN string into position components without raising exceptions.
74
75
  #
75
- # @param feen_string [String] FEEN string to validate
76
- # @return [Boolean] True if the string is a valid FEEN string, false otherwise
77
- # @example
78
- # Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -") # => true
79
- # Feen.valid?("invalid feen string") # => false
80
- def self.valid?(feen_string)
81
- parse(feen_string)
82
- true
83
- rescue ::ArgumentError
84
- false
85
- end
86
-
87
- # Converts a FEN string to a FEEN string for chess positions
76
+ # This method works like `parse` but returns nil instead of raising an exception
77
+ # if the FEEN string is invalid.
88
78
  #
89
- # @param fen_string [String] Standard FEN notation string for chess
90
- # @return [String] Equivalent FEEN notation string
91
- # @raise [ArgumentError] If the FEN string is invalid
79
+ # @param feen_string [String] Complete FEEN notation string
80
+ # @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
92
81
  # @example
93
- # Feen.from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
94
- # # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -"
95
- def self.from_fen(fen_string)
96
- Converter.from_fen(fen_string)
82
+ # # Valid FEEN string
83
+ # Feen.safe_parse("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
84
+ # # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
85
+ #
86
+ # # Invalid FEEN string
87
+ # Feen.safe_parse("invalid feen string")
88
+ # # => nil
89
+ def self.safe_parse(feen_string)
90
+ Parser.safe_parse(feen_string)
97
91
  end
98
92
 
99
- # Converts a FEEN string to a FEN string for chess positions
93
+ # Validates if the given string is a valid and canonical FEEN string
100
94
  #
101
- # @param feen_string [String] FEEN notation string
102
- # @return [String] Equivalent FEN notation string
103
- # @raise [ArgumentError] If the FEEN string is invalid
95
+ # This method performs a complete validation in two steps:
96
+ # 1. Syntax check: Verifies the string can be parsed as FEEN
97
+ # 2. Canonicity check: Ensures the string is in canonical form by comparing
98
+ # it with a freshly generated FEEN string created from its parsed components
99
+ #
100
+ # This approach guarantees that the string not only follows FEEN syntax
101
+ # but is also in its most compact, canonical representation.
102
+ #
103
+ # @param feen_string [String] FEEN string to validate
104
+ # @return [Boolean] True if the string is a valid and canonical FEEN string
104
105
  # @example
105
- # Feen.to_fen("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -")
106
- # # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
107
- def self.to_fen(feen_string)
108
- Converter.to_fen(feen_string)
106
+ # # Canonical form
107
+ # Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi") # => true
108
+ #
109
+ # # Invalid syntax
110
+ # Feen.valid?("invalid feen string") # => false
111
+ #
112
+ # # Valid syntax but non-canonical form (pieces in hand not in lexicographic order)
113
+ # Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi") # => false
114
+ def self.valid?(feen_string)
115
+ # First check: Basic syntax validation
116
+ begin
117
+ parsed_data = parse(feen_string)
118
+ rescue ::ArgumentError
119
+ return false
120
+ end
121
+
122
+ # Second check: Canonicity validation through round-trip conversion
123
+ # Generate a fresh FEEN string from the parsed data
124
+ canonical_feen = dump(**parsed_data)
125
+
126
+ # Compare the original string with the canonical form
127
+ feen_string == canonical_feen
109
128
  end
110
129
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feen
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0.beta2
4
+ version: 5.0.0.beta4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -10,6 +10,9 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: A Ruby interface for data serialization and deserialization in FEEN format.
13
+ FEEN is a compact, canonical, and rule-agnostic textual format for representing
14
+ static board positions in two-player piece-placement games like Chess, Shogi, Xiangqi,
15
+ and others.
13
16
  email: contact@cyril.email
14
17
  executables: []
15
18
  extensions: []
@@ -18,23 +21,34 @@ files:
18
21
  - LICENSE.md
19
22
  - README.md
20
23
  - lib/feen.rb
21
- - lib/feen/converter.rb
22
- - lib/feen/converter/from_fen.rb
23
- - lib/feen/converter/to_fen.rb
24
24
  - lib/feen/dumper.rb
25
25
  - lib/feen/dumper/games_turn.rb
26
26
  - lib/feen/dumper/piece_placement.rb
27
27
  - lib/feen/dumper/pieces_in_hand.rb
28
+ - lib/feen/dumper/pieces_in_hand/errors.rb
29
+ - lib/feen/dumper/pieces_in_hand/no_pieces.rb
28
30
  - lib/feen/parser.rb
29
31
  - lib/feen/parser/games_turn.rb
32
+ - lib/feen/parser/games_turn/errors.rb
33
+ - lib/feen/parser/games_turn/valid_games_turn_pattern.rb
30
34
  - lib/feen/parser/piece_placement.rb
31
35
  - lib/feen/parser/pieces_in_hand.rb
32
- - lib/feen/sanitizer.rb
36
+ - lib/feen/parser/pieces_in_hand/errors.rb
37
+ - lib/feen/parser/pieces_in_hand/no_pieces.rb
38
+ - lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
39
+ - lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
33
40
  homepage: https://github.com/sashite/feen.rb
34
41
  licenses:
35
42
  - MIT
36
43
  metadata:
44
+ bug_tracker_uri: https://github.com/sashite/feen.rb/issues
45
+ documentation_uri: https://rubydoc.info/github/sashite/feen.rb/main
46
+ homepage_uri: https://github.com/sashite/feen.rb
47
+ source_code_uri: https://github.com/sashite/feen.rb
48
+ specification_uri: https://sashite.dev/documents/feen/1.0.0/
49
+ funding_uri: https://github.com/sponsors/cyril
37
50
  rubygems_mfa_required: 'true'
51
+ article_uri: https://blog.cyril.email/posts/2025-05-01/introducing-feen-notation.html
38
52
  rdoc_options: []
39
53
  require_paths:
40
54
  - lib
@@ -42,7 +56,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
42
56
  requirements:
43
57
  - - ">="
44
58
  - !ruby/object:Gem::Version
45
- version: 3.4.0
59
+ version: 3.2.0
46
60
  required_rubygems_version: !ruby/object:Gem::Requirement
47
61
  requirements:
48
62
  - - ">="
@@ -51,5 +65,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
65
  requirements: []
52
66
  rubygems_version: 3.6.7
53
67
  specification_version: 4
54
- summary: FEEN support for the Ruby language.
68
+ summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
55
69
  test_files: []