feen 5.0.0.beta3 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad6a4426ae68ac5344888465a23eea36cf28b3e0137b50af8483b98288012969
4
- data.tar.gz: f9f42840240c5b75629cdb5459f492f966d3fc2706a354c18abc9841a9f17e21
3
+ metadata.gz: de87bfc9b071e9f1ff14b6f7378ba0a6f9f78cdb9a9bfe32ca00f2494a1be2cd
4
+ data.tar.gz: fd4d19e8a3b7fb845f081b127a859ff0791c8c11c05e1efb4846152044f67c18
5
5
  SHA512:
6
- metadata.gz: 31141ae8b866a11ffec6eed44308b475d03e75174ec0bc12b14ff0dc60bfe8b21ad60af3d32e1747d7dce94fe0cff0bbd5add502f26c26aeffac97104fbeeaca
7
- data.tar.gz: 82c82de3cf0024e596d9f318e612a8077069a9b519e6b4e753dc808bbee91045f72328489a26307f0877194a4e99ffc8502dd3d3d1610900215ab697fa479ce4
6
+ metadata.gz: e9fe95297b8577a3bf171a865deca0dc9c2eed91a7d039c04f97077d53d3fa912ca553f28f448f0dd987087926d3ca5225624d7f2d8d08a9275a16a11efe26e0
7
+ data.tar.gz: b82b7e00f5ece6f5e5859750226be3c74db23e694c8e53def8c8c656fe2fb3ecba7f0f4166cf3d9a4f6c7b4ab7af3cf6544da87a2684e438a389d96e04b3dc75
data/README.md CHANGED
@@ -5,23 +5,24 @@
5
5
  ![Ruby](https://github.com/sashite/feen.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
6
  [![License](https://img.shields.io/github/license/sashite/feen.rb?label=License&logo=github)](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
7
7
 
8
- > **FEEN** (Format for Encounter & Entertainment Notation) support for the Ruby language.
8
+ > **FEEN** (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
9
9
 
10
10
  ## What is FEEN?
11
11
 
12
- FEEN (Format for Encounter & Entertainment Notation) is a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
12
+ FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
13
13
 
14
14
  This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/), providing a Ruby interface for:
15
15
  - Representing positions from various games without knowledge of specific rules
16
16
  - Supporting boards of arbitrary dimensions
17
17
  - Encoding pieces in hand (as used in Shogi)
18
18
  - Facilitating serialization and deserialization of positions
19
+ - Ensuring canonical representation for consistent data handling
19
20
 
20
21
  ## Installation
21
22
 
22
23
  ```ruby
23
24
  # In your Gemfile
24
- gem "feen", ">= 5.0.0.beta3"
25
+ gem "feen", ">= 5.0.0.beta4"
25
26
  ```
26
27
 
27
28
  Or install manually:
@@ -52,7 +53,7 @@ position = Feen.parse(feen_string)
52
53
 
53
54
  # Result is a hash:
54
55
  # {
55
- # "piece_placement" => [
56
+ # piece_placement: [
56
57
  # ["r", "n", "b", "q", "k=", "b", "n", "r"],
57
58
  # ["p", "p", "p", "p", "p", "p", "p", "p"],
58
59
  # ["", "", "", "", "", "", "", ""],
@@ -62,11 +63,27 @@ position = Feen.parse(feen_string)
62
63
  # ["P", "P", "P", "P", "P", "P", "P", "P"],
63
64
  # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
64
65
  # ],
65
- # "games_turn" => ["CHESS", "chess"],
66
- # "pieces_in_hand" => []
66
+ # pieces_in_hand: [],
67
+ # games_turn: ["CHESS", "chess"]
67
68
  # }
68
69
  ```
69
70
 
71
+ ### Safe Parsing
72
+
73
+ Parse a FEEN string without raising exceptions:
74
+
75
+ ```ruby
76
+ require "feen"
77
+
78
+ # Valid FEEN string
79
+ result = Feen.safe_parse("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
80
+ # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
81
+
82
+ # Invalid FEEN string
83
+ result = Feen.safe_parse("invalid feen string")
84
+ # => nil
85
+ ```
86
+
70
87
  ### Creating FEEN Strings
71
88
 
72
89
  Convert position components to a FEEN string using named arguments:
@@ -96,18 +113,28 @@ result = Feen.dump(
96
113
 
97
114
  ### Validation
98
115
 
99
- Check if a string is valid FEEN notation:
116
+ Check if a string is valid FEEN notation and in canonical form:
100
117
 
101
118
  ```ruby
102
119
  require "feen"
103
120
 
104
- Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
121
+ # Canonical form
122
+ Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi")
105
123
  # => true
106
124
 
125
+ # Invalid syntax
107
126
  Feen.valid?("invalid feen string")
108
127
  # => false
128
+
129
+ # Valid syntax but non-canonical form (pieces in hand not in lexicographic order)
130
+ Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi")
131
+ # => false
109
132
  ```
110
133
 
134
+ The `valid?` method performs two levels of validation:
135
+ 1. **Syntax check**: Verifies the string can be parsed as FEEN
136
+ 2. **Canonicity check**: Ensures the string is in its canonical form through round-trip conversion
137
+
111
138
  ## Game Examples
112
139
 
113
140
  As FEEN is rule-agnostic, it can represent positions from various board games. Here are some examples:
@@ -125,14 +152,14 @@ In this initial chess position:
125
152
  ### Shogi (Japanese Chess)
126
153
 
127
154
  ```ruby
128
- feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2g2snl SHOGI/shogi"
155
+ feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi"
129
156
  ```
130
157
 
131
158
  In this shogi position:
132
159
  - The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
133
160
  - The notation allows for pieces in hand, indicated in the second field
134
161
  - `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
135
- - `N5P2g2snl` shows the pieces in hand: Sente has a Knight (N) and 5 Pawns (P), while Gote has 2 Golds (g), 2 Silvers (s), a Knight (n), and a Lance (l)
162
+ - `N5P2gln2s` shows the pieces in hand: Sente has a Knight (N) and 5 Pawns (5P), while Gote has 2 Golds (2g), a Lance (l), a Knight (n), and 2 Silvers (2s), all properly sorted in ASCII lexicographic order
136
163
 
137
164
  ### Makruk (Thai Chess)
138
165
 
data/lib/feen/dumper.rb CHANGED
@@ -6,7 +6,7 @@ require_relative File.join("dumper", "pieces_in_hand")
6
6
 
7
7
  module Feen
8
8
  # Module responsible for converting internal data structures to FEEN notation strings.
9
- # This implements the serialization part of the FEEN (Format for Encounter & Entertainment Notation) format.
9
+ # This implements the serialization part of the FEEN (Forsyth–Edwards Enhanced Notation) format.
10
10
  module Dumper
11
11
  # Field separator used between the three main components of FEEN notation
12
12
  FIELD_SEPARATOR = " "
@@ -7,7 +7,8 @@ module Feen
7
7
  Errors = {
8
8
  invalid_type: "Pieces in hand must be a string, got %s",
9
9
  empty_string: "Pieces in hand string cannot be empty",
10
- invalid_format: "Invalid pieces in hand format: %s"
10
+ invalid_format: "Invalid pieces in hand format: %s",
11
+ sorting_error: "Pieces in hand must be in ASCII lexicographic order"
11
12
  }.freeze
12
13
  end
13
14
  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
@@ -3,13 +3,27 @@
3
3
  module Feen
4
4
  module Parser
5
5
  module PiecesInHand
6
- # Valid pattern for pieces in hand based on BNF:
7
- # <pieces-in-hand> ::= "-" | <A-part> <B-part> ... <Z-part> <a-part> <b-part> ... <z-part>
8
- # where each part can be empty or contain repetitions of the same letter
9
- ValidFormatPattern = /\A(?:-|
10
- A*B*C*D*E*F*G*H*I*J*K*L*M*N*O*P*Q*R*S*T*U*V*W*X*Y*Z*
11
- a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*
12
- )\z/x
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
13
27
  end
14
28
  end
15
29
  end
@@ -1,32 +1,22 @@
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
10
  # Handles parsing of the pieces in hand section of a FEEN string.
6
11
  # Pieces in hand represent pieces available for dropping onto the board.
7
12
  module PiecesInHand
8
- # Character used to represent no pieces in hand
9
- NO_PIECES = "-"
10
-
11
- # Error messages for validation
12
- ERRORS = {
13
- invalid_type: "Pieces in hand must be a string, got %s",
14
- empty_string: "Pieces in hand string cannot be empty",
15
- invalid_format: "Invalid pieces in hand format: %s",
16
- sorting_error: "Pieces in hand must be in ASCII lexicographic order"
17
- }.freeze
18
-
19
- # Valid pattern for pieces in hand based on BNF:
20
- # <pieces-in-hand> ::= "-" | <piece> <pieces-in-hand>
21
- # <piece> ::= [a-zA-Z]
22
- VALID_FORMAT_PATTERN = /\A(?:-|[a-zA-Z]+)\z/
23
-
24
13
  # Parses the pieces in hand section of a FEEN string.
25
14
  #
26
15
  # @param pieces_in_hand_str [String] FEEN pieces in hand string
27
16
  # @return [Array<String>] Array of single-character piece identifiers in the
28
- # format specified in the FEEN string (no prefixes or suffixes), sorted in ASCII
29
- # lexicographic order. Empty array if no pieces are in hand.
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.
30
20
  # @raise [ArgumentError] If the input string is invalid
31
21
  #
32
22
  # @example Parse no pieces in hand
@@ -34,18 +24,26 @@ module Feen
34
24
  # # => []
35
25
  #
36
26
  # @example Parse multiple pieces in hand
37
- # PiecesInHand.parse("BNPPb")
27
+ # PiecesInHand.parse("BN2Pb")
38
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"]
39
33
  def self.parse(pieces_in_hand_str)
34
+ # Validate input
40
35
  validate_input_type(pieces_in_hand_str)
41
36
  validate_format(pieces_in_hand_str)
42
37
 
43
- return [] if pieces_in_hand_str == NO_PIECES
38
+ # Handle the no-pieces case early
39
+ return [] if pieces_in_hand_str == NoPieces
44
40
 
45
- pieces = pieces_in_hand_str.chars
46
- validate_pieces_order(pieces)
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)
47
44
 
48
- pieces
45
+ # Expand the pieces into an array and maintain lexicographic ordering
46
+ expand_pieces(pieces_with_counts)
49
47
  end
50
48
 
51
49
  # Validates that the input is a non-empty string.
@@ -54,30 +52,68 @@ module Feen
54
52
  # @raise [ArgumentError] If input is not a string or is empty
55
53
  # @return [void]
56
54
  private_class_method def self.validate_input_type(str)
57
- raise ::ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(::String)
58
- raise ::ArgumentError, ERRORS[:empty_string] if str.empty?
55
+ raise ::ArgumentError, format(Errors[:invalid_type], str.class) unless str.is_a?(::String)
56
+ raise ::ArgumentError, Errors[:empty_string] if str.empty?
59
57
  end
60
58
 
61
- # Validates that the input string matches the expected format.
59
+ # Validates that the input string matches the expected format according to FEEN specification.
62
60
  #
63
61
  # @param str [String] Input string to validate
64
62
  # @raise [ArgumentError] If format is invalid
65
63
  # @return [void]
66
64
  private_class_method def self.validate_format(str)
67
- return if str.match?(VALID_FORMAT_PATTERN)
65
+ return if str == NoPieces || str.match?(ValidFormatPattern)
68
66
 
69
- raise ::ArgumentError, format(ERRORS[:invalid_format], str)
67
+ raise ::ArgumentError, format(Errors[:invalid_format], str)
70
68
  end
71
69
 
72
- # Validates that pieces are sorted in ASCII lexicographic order.
70
+ # Extracts pieces with their counts from the FEEN string.
73
71
  #
74
- # @param pieces [Array<String>] Array of piece identifiers
75
- # @raise [ArgumentError] If pieces are not sorted
72
+ # @param str [String] FEEN pieces in hand 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
81
+
82
+ count_str, piece = match.captures
83
+ count = count_str ? count_str.to_i : 1
84
+
85
+ # Add to our result with piece type and count
86
+ result << { piece: piece, count: count }
87
+
88
+ # Move position forward
89
+ position += match[0].length
90
+ end
91
+
92
+ result
93
+ end
94
+
95
+ # Validates that pieces are in lexicographic ASCII order.
96
+ #
97
+ # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
98
+ # @raise [ArgumentError] If pieces are not in lexicographic order
76
99
  # @return [void]
77
- private_class_method def self.validate_pieces_order(pieces)
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
78
104
  return if pieces == pieces.sort
79
105
 
80
- raise ::ArgumentError, ERRORS[:sorting_error]
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
81
117
  end
82
118
  end
83
119
  end
data/lib/feen/parser.rb CHANGED
@@ -6,7 +6,7 @@ require_relative File.join("parser", "pieces_in_hand")
6
6
 
7
7
  module Feen
8
8
  # Module responsible for parsing FEEN notation strings into internal data structures.
9
- # FEEN (Format for Encounter & Entertainment Notation) is a compact, canonical, and rule-agnostic
9
+ # FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic
10
10
  # textual format for representing static board positions in two-player piece-placement games.
11
11
  module Parser
12
12
  # Regular expression pattern for matching a valid FEEN string structure
@@ -23,7 +23,7 @@ module Feen
23
23
  # - :piece_placement [Array] - Hierarchical array structure representing the board
24
24
  # - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
25
25
  # - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
26
- # @raise [ArgumentError] If the FEEN string is invalid or any component cannot be parsed
26
+ # @raise [ArgumentError] If the FEEN string is invalid
27
27
  #
28
28
  # @example Parsing a standard chess initial position
29
29
  # feen = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
@@ -42,25 +42,6 @@ module Feen
42
42
  # # pieces_in_hand: [],
43
43
  # # games_turn: ["CHESS", "chess"]
44
44
  # # }
45
- #
46
- # @example Parsing a shogi position (from a Tempo Loss Bishop Exchange opening) with pieces in hand
47
- # feen = "lnsgk2nl/1r4gs1/p1pppp1pp/1p4p2/7P1/2P6/PP1PPPP1P/1SG4R1/LN2KGSNL Bb SHOGI/shogi"
48
- # result = Feen::Parser.parse(feen)
49
- # # => {
50
- # # piece_placement: [
51
- # # ["l", "n", "s", "g", "k", "", "", "n", "l"],
52
- # # ["", "r", "", "", "", "", "g", "s", ""],
53
- # # ["p", "", "p", "p", "p", "p", "", "p", "p"],
54
- # # ["", "p", "", "", "", "", "p", "", ""],
55
- # # ["", "", "", "", "", "", "", "P", ""],
56
- # # ["", "", "P", "", "", "", "", "", ""],
57
- # # ["P", "P", "", "P", "P", "P", "P", "", "P"],
58
- # # ["", "S", "G", "", "", "", "", "R", ""],
59
- # # ["L", "N", "", "", "K", "G", "S", "N", "L"]
60
- # # ],
61
- # # pieces_in_hand: ["B", "b"],
62
- # # games_turn: ["SHOGI", "shogi"]
63
- # # }
64
45
  def self.parse(feen_string)
65
46
  feen_string = String(feen_string)
66
47
 
@@ -85,5 +66,28 @@ module Feen
85
66
  games_turn:
86
67
  }
87
68
  end
69
+
70
+ # Safely parses a complete FEEN string into a structured representation without raising exceptions
71
+ #
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
91
+ end
88
92
  end
89
93
  end
data/lib/feen.rb CHANGED
@@ -6,11 +6,19 @@ require_relative File.join("feen", "parser")
6
6
  # This module provides a Ruby interface for data serialization and
7
7
  # deserialization in FEEN format.
8
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
+ #
9
13
  # @see https://sashite.dev/documents/feen/1.0.0/
10
14
  module Feen
11
15
  # Dumps position components into a FEEN string.
12
16
  #
13
- # @see Feen::Dumper.dump for the detailed parameters documentation
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
14
22
  # @return [String] FEEN notation string
15
23
  # @raise [ArgumentError] If any parameter is invalid
16
24
  # @example
@@ -30,14 +38,17 @@ module Feen
30
38
  # games_turn: ["CHESS", "chess"]
31
39
  # )
32
40
  # # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
33
- def self.dump(...)
34
- Dumper.dump(...)
41
+ def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
42
+ Dumper.dump(piece_placement:, pieces_in_hand:, games_turn:)
35
43
  end
36
44
 
37
45
  # Parses a FEEN string into position components.
38
46
  #
39
- # @see Feen::Parser.parse for the detailed parameters and return value documentation
40
- # @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]
41
52
  # @raise [ArgumentError] If the FEEN string is invalid
42
53
  # @example
43
54
  # feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
@@ -56,21 +67,63 @@ module Feen
56
67
  # # pieces_in_hand: [],
57
68
  # # games_turn: ["CHESS", "chess"]
58
69
  # # }
59
- def self.parse(...)
60
- Parser.parse(...)
70
+ def self.parse(feen_string)
71
+ Parser.parse(feen_string)
61
72
  end
62
73
 
63
- # Validates if the given string is a valid FEEN string
74
+ # Safely parses a FEEN string into position components without raising exceptions.
75
+ #
76
+ # This method works like `parse` but returns nil instead of raising an exception
77
+ # if the FEEN string is invalid.
64
78
  #
65
- # @see Feen.parse for parameter details
66
- # @return [Boolean] True if the string is a valid FEEN string, false otherwise
79
+ # @param feen_string [String] Complete FEEN notation string
80
+ # @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
67
81
  # @example
68
- # Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess") # => true
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)
91
+ end
92
+
93
+ # Validates if the given string is a valid and canonical FEEN string
94
+ #
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
105
+ # @example
106
+ # # Canonical form
107
+ # Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi") # => true
108
+ #
109
+ # # Invalid syntax
69
110
  # Feen.valid?("invalid feen string") # => false
70
- def self.valid?(...)
71
- parse(...)
72
- true
73
- rescue ::ArgumentError
74
- 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
75
128
  end
76
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.beta3
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: []
@@ -32,12 +35,20 @@ files:
32
35
  - lib/feen/parser/pieces_in_hand.rb
33
36
  - lib/feen/parser/pieces_in_hand/errors.rb
34
37
  - lib/feen/parser/pieces_in_hand/no_pieces.rb
38
+ - lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
35
39
  - lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
36
40
  homepage: https://github.com/sashite/feen.rb
37
41
  licenses:
38
42
  - MIT
39
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
40
50
  rubygems_mfa_required: 'true'
51
+ article_uri: https://blog.cyril.email/posts/2025-05-01/introducing-feen-notation.html
41
52
  rdoc_options: []
42
53
  require_paths:
43
54
  - lib
@@ -54,5 +65,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
65
  requirements: []
55
66
  rubygems_version: 3.6.7
56
67
  specification_version: 4
57
- summary: FEEN support for the Ruby language.
68
+ summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
58
69
  test_files: []