feen 5.0.0.beta9 → 5.0.0.beta10
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 +4 -4
- data/README.md +264 -76
- data/lib/feen/dumper/pieces_in_hand.rb +101 -40
- data/lib/feen/dumper/style_turn.rb +71 -0
- data/lib/feen/dumper.rb +17 -17
- data/lib/feen/parser/pieces_in_hand.rb +49 -72
- data/lib/feen/parser/style_turn.rb +101 -0
- data/lib/feen/parser.rb +8 -8
- data/lib/feen.rb +27 -27
- metadata +33 -7
- data/lib/feen/dumper/games_turn.rb +0 -70
- data/lib/feen/parser/games_turn.rb +0 -78
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sashite-snn"
|
4
|
+
|
5
|
+
module Feen
|
6
|
+
module Parser
|
7
|
+
# Handles parsing of the style turn section of a FEEN string
|
8
|
+
module StyleTurn
|
9
|
+
# Error messages for style turn parsing
|
10
|
+
ERRORS = {
|
11
|
+
invalid_type: "Style turn must be a string, got %s",
|
12
|
+
empty_string: "Style turn string cannot be empty",
|
13
|
+
invalid_format: "Invalid style turn format. Expected format: UPPERCASE/lowercase or lowercase/UPPERCASE",
|
14
|
+
invalid_snn: "Invalid SNN notation in style turn: %s"
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
# Pattern matching the FEEN specification for style turn
|
18
|
+
# <style-turn> ::= <style-id-uppercase> "/" <style-id-lowercase>
|
19
|
+
# | <style-id-lowercase> "/" <style-id-uppercase>
|
20
|
+
VALID_STYLE_TURN_PATTERN = %r{
|
21
|
+
\A # Start of string
|
22
|
+
(?: # Non-capturing group for alternatives
|
23
|
+
(?<uppercase_first>[A-Z][A-Z0-9]*) # Named group: uppercase style identifier first
|
24
|
+
/ # Separator
|
25
|
+
(?<lowercase_second>[a-z][a-z0-9]*) # Named group: lowercase style identifier second
|
26
|
+
| # OR
|
27
|
+
(?<lowercase_first>[a-z][a-z0-9]*) # Named group: lowercase style identifier first
|
28
|
+
/ # Separator
|
29
|
+
(?<uppercase_second>[A-Z][A-Z0-9]*) # Named group: uppercase style identifier second
|
30
|
+
)
|
31
|
+
\z # End of string
|
32
|
+
}x
|
33
|
+
|
34
|
+
# Parses the style turn section of a FEEN string
|
35
|
+
#
|
36
|
+
# @param style_turn_str [String] FEEN style turn string
|
37
|
+
# @return [Array<String>] Array containing [active_style, inactive_style]
|
38
|
+
# @raise [ArgumentError] If the input string is invalid
|
39
|
+
#
|
40
|
+
# @example Valid style turn string with uppercase first
|
41
|
+
# StyleTurn.parse("CHESS/shogi")
|
42
|
+
# # => ["CHESS", "shogi"]
|
43
|
+
#
|
44
|
+
# @example Valid style turn string with lowercase first
|
45
|
+
# StyleTurn.parse("chess/SHOGI")
|
46
|
+
# # => ["chess", "SHOGI"]
|
47
|
+
#
|
48
|
+
# @example Valid style turn with numeric identifiers
|
49
|
+
# StyleTurn.parse("CHESS960/makruk")
|
50
|
+
# # => ["CHESS960", "makruk"]
|
51
|
+
def self.parse(style_turn_str)
|
52
|
+
validate_input_type(style_turn_str)
|
53
|
+
|
54
|
+
match = VALID_STYLE_TURN_PATTERN.match(style_turn_str)
|
55
|
+
raise ::ArgumentError, ERRORS[:invalid_format] unless match
|
56
|
+
|
57
|
+
style_identifiers = extract_style_identifiers(match)
|
58
|
+
validate_snn_compliance(style_identifiers)
|
59
|
+
|
60
|
+
style_identifiers
|
61
|
+
end
|
62
|
+
|
63
|
+
# Validates that the input is a non-empty string
|
64
|
+
#
|
65
|
+
# @param str [String] Input string to validate
|
66
|
+
# @raise [ArgumentError] If input is not a string or is empty
|
67
|
+
# @return [void]
|
68
|
+
private_class_method def self.validate_input_type(str)
|
69
|
+
raise ::ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(::String)
|
70
|
+
raise ::ArgumentError, ERRORS[:empty_string] if str.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
# Extracts style identifiers from regexp match captures
|
74
|
+
#
|
75
|
+
# @param match [MatchData] Regexp match data with named captures
|
76
|
+
# @return [Array<String>] Array containing [active_style, inactive_style]
|
77
|
+
private_class_method def self.extract_style_identifiers(match)
|
78
|
+
captures = match.named_captures
|
79
|
+
|
80
|
+
if captures["uppercase_first"]
|
81
|
+
[captures["uppercase_first"], captures["lowercase_second"]]
|
82
|
+
else
|
83
|
+
[captures["lowercase_first"], captures["uppercase_second"]]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Validates that both style identifiers comply with SNN specification
|
88
|
+
#
|
89
|
+
# @param identifiers [Array<String>] Array of style identifiers to validate
|
90
|
+
# @raise [ArgumentError] If any identifier is invalid SNN notation
|
91
|
+
# @return [void]
|
92
|
+
private_class_method def self.validate_snn_compliance(identifiers)
|
93
|
+
identifiers.each do |identifier|
|
94
|
+
# Validate using the sashite-snn gem
|
95
|
+
# @see https://rubygems.org/gems/sashite-snn
|
96
|
+
raise ::ArgumentError, format(ERRORS[:invalid_snn], identifier) unless ::Sashite::Snn.valid?(identifier)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/feen/parser.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("parser", "
|
3
|
+
require_relative File.join("parser", "style_turn")
|
4
4
|
require_relative File.join("parser", "piece_placement")
|
5
5
|
require_relative File.join("parser", "pieces_in_hand")
|
6
6
|
|
@@ -21,8 +21,8 @@ module Feen
|
|
21
21
|
# @param feen_string [String] Complete FEEN notation string
|
22
22
|
# @return [Hash] Hash containing the parsed position data with the following keys:
|
23
23
|
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
24
|
-
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board (
|
25
|
-
# - :
|
24
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board (may include modifiers)
|
25
|
+
# - :style_turn [Array<String>] - A two-element array with [active_style, inactive_style]
|
26
26
|
# @raise [ArgumentError] If the FEEN string is invalid
|
27
27
|
#
|
28
28
|
# @example Parsing a standard chess initial position
|
@@ -40,7 +40,7 @@ module Feen
|
|
40
40
|
# # ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
41
41
|
# # ],
|
42
42
|
# # pieces_in_hand: [],
|
43
|
-
# #
|
43
|
+
# # style_turn: ["CHESS", "chess"]
|
44
44
|
# # }
|
45
45
|
def self.parse(feen_string)
|
46
46
|
feen_string = String(feen_string)
|
@@ -52,18 +52,18 @@ module Feen
|
|
52
52
|
raise ::ArgumentError, INVALID_FORMAT_ERROR unless match
|
53
53
|
|
54
54
|
# Capture the three distinct parts
|
55
|
-
piece_placement_string, pieces_in_hand_string,
|
55
|
+
piece_placement_string, pieces_in_hand_string, style_turn_string = match.captures
|
56
56
|
|
57
57
|
# Parse each field using the appropriate submodule
|
58
58
|
piece_placement = PiecePlacement.parse(piece_placement_string)
|
59
59
|
pieces_in_hand = PiecesInHand.parse(pieces_in_hand_string)
|
60
|
-
|
60
|
+
style_turn = StyleTurn.parse(style_turn_string)
|
61
61
|
|
62
62
|
# Create a structured representation of the position
|
63
63
|
{
|
64
64
|
piece_placement:,
|
65
65
|
pieces_in_hand:,
|
66
|
-
|
66
|
+
style_turn:
|
67
67
|
}
|
68
68
|
end
|
69
69
|
|
@@ -78,7 +78,7 @@ module Feen
|
|
78
78
|
# @example Parsing a valid FEEN string
|
79
79
|
# feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
|
80
80
|
# result = Feen::Parser.safe_parse(feen)
|
81
|
-
# # => {piece_placement: [...], pieces_in_hand: [...],
|
81
|
+
# # => {piece_placement: [...], pieces_in_hand: [...], style_turn: [...]}
|
82
82
|
#
|
83
83
|
# @example Parsing an invalid FEEN string
|
84
84
|
# feen = "invalid feen string"
|
data/lib/feen.rb
CHANGED
@@ -17,11 +17,11 @@ module Feen
|
|
17
17
|
# @param piece_placement [Array] Board position data structure representing the spatial
|
18
18
|
# distribution of pieces across the board
|
19
19
|
# @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board.
|
20
|
-
#
|
21
|
-
# @param
|
22
|
-
# active player's
|
20
|
+
# May include PNN modifiers (per FEEN v1.0.0 specification)
|
21
|
+
# @param style_turn [Array<String>] A two-element array where the first element is the
|
22
|
+
# active player's style and the second is the inactive player's style
|
23
23
|
# @return [String] FEEN notation string
|
24
|
-
# @raise [ArgumentError] If any parameter is invalid
|
24
|
+
# @raise [ArgumentError] If any parameter is invalid
|
25
25
|
# @example
|
26
26
|
# piece_placement = [
|
27
27
|
# ["r", "n", "b", "q", "k", "b", "n", "r"],
|
@@ -36,11 +36,11 @@ module Feen
|
|
36
36
|
# Feen.dump(
|
37
37
|
# piece_placement: piece_placement,
|
38
38
|
# pieces_in_hand: [],
|
39
|
-
#
|
39
|
+
# style_turn: ["CHESS", "chess"]
|
40
40
|
# )
|
41
41
|
# # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
|
42
|
-
def self.dump(piece_placement:, pieces_in_hand:,
|
43
|
-
Dumper.dump(piece_placement:, pieces_in_hand:,
|
42
|
+
def self.dump(piece_placement:, pieces_in_hand:, style_turn:)
|
43
|
+
Dumper.dump(piece_placement:, pieces_in_hand:, style_turn:)
|
44
44
|
end
|
45
45
|
|
46
46
|
# Parses a FEEN string into position components.
|
@@ -48,8 +48,8 @@ module Feen
|
|
48
48
|
# @param feen_string [String] Complete FEEN notation string
|
49
49
|
# @return [Hash] Hash containing the parsed position data with the following keys:
|
50
50
|
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
51
|
-
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board (
|
52
|
-
# - :
|
51
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board (may include modifiers)
|
52
|
+
# - :style_turn [Array<String>] - A two-element array with [active_style, inactive_style]
|
53
53
|
# @raise [ArgumentError] If the FEEN string is invalid
|
54
54
|
# @example
|
55
55
|
# feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
|
@@ -66,7 +66,7 @@ module Feen
|
|
66
66
|
# # ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
67
67
|
# # ],
|
68
68
|
# # pieces_in_hand: [],
|
69
|
-
# #
|
69
|
+
# # style_turn: ["CHESS", "chess"]
|
70
70
|
# # }
|
71
71
|
def self.parse(feen_string)
|
72
72
|
Parser.parse(feen_string)
|
@@ -82,7 +82,7 @@ module Feen
|
|
82
82
|
# @example
|
83
83
|
# # Valid FEEN string
|
84
84
|
# Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess")
|
85
|
-
# # => {piece_placement: [...], pieces_in_hand: [...],
|
85
|
+
# # => {piece_placement: [...], pieces_in_hand: [...], style_turn: [...]}
|
86
86
|
#
|
87
87
|
# # Invalid FEEN string
|
88
88
|
# Feen.safe_parse("invalid feen string")
|
@@ -101,12 +101,15 @@ module Feen
|
|
101
101
|
# This approach guarantees that the string not only follows FEEN syntax
|
102
102
|
# but is also in its most compact, canonical representation.
|
103
103
|
#
|
104
|
-
# According to FEEN specification:
|
105
|
-
# - Pieces in hand
|
106
|
-
# - Pieces in hand must be sorted canonically
|
107
|
-
# 1. By
|
108
|
-
# 2. By
|
109
|
-
#
|
104
|
+
# According to FEEN v1.0.0 specification:
|
105
|
+
# - Pieces in hand may include PNN modifiers (prefixes and suffixes)
|
106
|
+
# - Pieces in hand must be sorted canonically according to the sorting algorithm:
|
107
|
+
# 1. By player (uppercase/lowercase separated by '/')
|
108
|
+
# 2. By quantity (descending)
|
109
|
+
# 3. By base letter (ascending)
|
110
|
+
# 4. By prefix (specific order: '-', '+', then no prefix)
|
111
|
+
# 5. By suffix (specific order: no suffix, then "'")
|
112
|
+
# - Style identifiers must follow SNN specification with semantic casing
|
110
113
|
#
|
111
114
|
# @param feen_string [String] FEEN string to validate
|
112
115
|
# @return [Boolean] True if the string is a valid and canonical FEEN string
|
@@ -117,24 +120,21 @@ module Feen
|
|
117
120
|
# # Invalid syntax
|
118
121
|
# Feen.valid?("invalid feen string") # => false
|
119
122
|
#
|
120
|
-
# # Valid syntax but non-canonical form (pieces in hand with modifiers)
|
121
|
-
# Feen.valid?("8/8/8/8/8/8/8/8 +P/ FOO/bar") # => false
|
122
|
-
#
|
123
123
|
# # Valid syntax but non-canonical form (wrong ordering in pieces in hand)
|
124
124
|
# Feen.valid?("8/8/8/8/8/8/8/8 P3K/ FOO/bar") # => false
|
125
|
+
#
|
126
|
+
# # Canonical form with modifiers in pieces in hand
|
127
|
+
# Feen.valid?("8/8/8/8/8/8/8/8 2+B5BK3-P-P'3+P'9PR2SS'/ FOO/bar") # => true
|
125
128
|
def self.valid?(feen_string)
|
126
129
|
# First check: Basic syntax validation
|
127
|
-
|
128
|
-
|
129
|
-
rescue ::ArgumentError
|
130
|
-
return false
|
131
|
-
end
|
130
|
+
parsed_data = safe_parse(feen_string)
|
131
|
+
return false if parsed_data.nil?
|
132
132
|
|
133
133
|
# Second check: Canonicity validation through round-trip conversion
|
134
134
|
# Generate a fresh FEEN string from the parsed data
|
135
135
|
canonical_feen = dump(**parsed_data)
|
136
136
|
|
137
|
-
# Compare the
|
138
|
-
|
137
|
+
# Compare the canonical form with the original string
|
138
|
+
canonical_feen == feen_string
|
139
139
|
end
|
140
140
|
end
|
metadata
CHANGED
@@ -1,17 +1,45 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0.0.
|
4
|
+
version: 5.0.0.beta10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: pnn
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 2.0.0
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 2.0.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sashite-snn
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.0.0
|
12
40
|
description: A Ruby interface for data serialization and deserialization in FEEN format.
|
13
41
|
FEEN is a compact, canonical, and rule-agnostic textual format for representing
|
14
|
-
static board positions in two-player piece-placement games like Chess,
|
42
|
+
static board positions in two-player piece-placement games like Chess, Shōgi, Xiangqi,
|
15
43
|
and others.
|
16
44
|
email: contact@cyril.email
|
17
45
|
executables: []
|
@@ -22,13 +50,13 @@ files:
|
|
22
50
|
- README.md
|
23
51
|
- lib/feen.rb
|
24
52
|
- lib/feen/dumper.rb
|
25
|
-
- lib/feen/dumper/games_turn.rb
|
26
53
|
- lib/feen/dumper/piece_placement.rb
|
27
54
|
- lib/feen/dumper/pieces_in_hand.rb
|
55
|
+
- lib/feen/dumper/style_turn.rb
|
28
56
|
- lib/feen/parser.rb
|
29
|
-
- lib/feen/parser/games_turn.rb
|
30
57
|
- lib/feen/parser/piece_placement.rb
|
31
58
|
- lib/feen/parser/pieces_in_hand.rb
|
59
|
+
- lib/feen/parser/style_turn.rb
|
32
60
|
homepage: https://github.com/sashite/feen.rb
|
33
61
|
licenses:
|
34
62
|
- MIT
|
@@ -39,8 +67,6 @@ metadata:
|
|
39
67
|
source_code_uri: https://github.com/sashite/feen.rb
|
40
68
|
specification_uri: https://sashite.dev/documents/feen/1.0.0/
|
41
69
|
rubygems_mfa_required: 'true'
|
42
|
-
keywords: board, board-games, chess, deserialization, feen, fen, game, makruk, notation,
|
43
|
-
serialization, shogi, xiangqi"
|
44
70
|
rdoc_options: []
|
45
71
|
require_paths:
|
46
72
|
- lib
|
@@ -1,70 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Dumper
|
5
|
-
# Handles conversion of games turn data to FEEN notation string
|
6
|
-
module GamesTurn
|
7
|
-
# Error messages for validation
|
8
|
-
ERRORS = {
|
9
|
-
invalid_type: "%s must be a String, got %s",
|
10
|
-
empty_string: "%s cannot be empty",
|
11
|
-
invalid_chars: "%s must contain only alphabetic characters (a-z, A-Z)",
|
12
|
-
mixed_case: "%s has mixed case: %s",
|
13
|
-
same_casing: "One variant must be uppercase and the other lowercase"
|
14
|
-
}.freeze
|
15
|
-
|
16
|
-
# Converts the active and inactive variant identifiers to a FEEN-formatted games turn string
|
17
|
-
#
|
18
|
-
# @param active_variant [String] Identifier for the player to move and their game variant
|
19
|
-
# @param inactive_variant [String] Identifier for the opponent and their game variant
|
20
|
-
# @return [String] FEEN-formatted games turn string
|
21
|
-
# @raise [ArgumentError] If the variant identifiers are invalid
|
22
|
-
#
|
23
|
-
# @example Valid games turn
|
24
|
-
# GamesTurn.dump("CHESS", "chess")
|
25
|
-
# # => "CHESS/chess"
|
26
|
-
#
|
27
|
-
# @example Invalid - same casing
|
28
|
-
# GamesTurn.dump("CHESS", "MAKRUK")
|
29
|
-
# # => ArgumentError: One variant must be uppercase and the other lowercase
|
30
|
-
def self.dump(active_variant, inactive_variant)
|
31
|
-
validate_variants(active_variant, inactive_variant)
|
32
|
-
"#{active_variant}/#{inactive_variant}"
|
33
|
-
end
|
34
|
-
|
35
|
-
# Validates the game variant identifiers
|
36
|
-
#
|
37
|
-
# @param active [String] The active player's variant identifier
|
38
|
-
# @param inactive [String] The inactive player's variant identifier
|
39
|
-
# @raise [ArgumentError] If the variant identifiers are invalid
|
40
|
-
# @return [void]
|
41
|
-
private_class_method def self.validate_variants(active, inactive)
|
42
|
-
# Validate basic type, presence and format
|
43
|
-
[["Active variant", active], ["Inactive variant", inactive]].each do |name, variant|
|
44
|
-
# Type validation
|
45
|
-
raise ArgumentError, format(ERRORS[:invalid_type], name, variant.class) unless variant.is_a?(String)
|
46
|
-
|
47
|
-
# Empty validation
|
48
|
-
raise ArgumentError, format(ERRORS[:empty_string], name) if variant.empty?
|
49
|
-
|
50
|
-
# Character validation
|
51
|
-
raise ArgumentError, format(ERRORS[:invalid_chars], name) unless variant.match?(/\A[a-zA-Z]+\z/)
|
52
|
-
|
53
|
-
# Mixed case validation
|
54
|
-
unless variant == variant.upcase || variant == variant.downcase
|
55
|
-
raise ArgumentError, format(ERRORS[:mixed_case], name, variant)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Casing difference validation
|
60
|
-
active_is_uppercase = active == active.upcase
|
61
|
-
inactive_is_uppercase = inactive == inactive.upcase
|
62
|
-
|
63
|
-
# Both variants must have different casing
|
64
|
-
return unless active_is_uppercase == inactive_is_uppercase
|
65
|
-
|
66
|
-
raise ArgumentError, ERRORS[:same_casing]
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Parser
|
5
|
-
# Handles parsing of the games turn section of a FEEN string
|
6
|
-
module GamesTurn
|
7
|
-
# Error messages for games turn parsing
|
8
|
-
ERRORS = {
|
9
|
-
invalid_type: "Games turn must be a string, got %s",
|
10
|
-
empty_string: "Games turn string cannot be empty",
|
11
|
-
invalid_format: "Invalid games turn format. Expected format: UPPERCASE/lowercase or lowercase/UPPERCASE"
|
12
|
-
}.freeze
|
13
|
-
|
14
|
-
# Pattern matching the FEEN specification for games turn
|
15
|
-
# <games-turn> ::= <game-id-uppercase> "/" <game-id-lowercase>
|
16
|
-
# | <game-id-lowercase> "/" <game-id-uppercase>
|
17
|
-
VALID_GAMES_TURN_PATTERN = %r{
|
18
|
-
\A # Start of string
|
19
|
-
(?: # Non-capturing group for alternatives
|
20
|
-
(?<uppercase_first>[A-Z]+) # Named group: uppercase identifier first
|
21
|
-
/ # Separator
|
22
|
-
(?<lowercase_second>[a-z]+) # Named group: lowercase identifier second
|
23
|
-
| # OR
|
24
|
-
(?<lowercase_first>[a-z]+) # Named group: lowercase identifier first
|
25
|
-
/ # Separator
|
26
|
-
(?<uppercase_second>[A-Z]+) # Named group: uppercase identifier second
|
27
|
-
)
|
28
|
-
\z # End of string
|
29
|
-
}x
|
30
|
-
|
31
|
-
# Parses the games turn section of a FEEN string
|
32
|
-
#
|
33
|
-
# @param games_turn_str [String] FEEN games turn string
|
34
|
-
# @return [Array<String>] Array containing [active_player, inactive_player]
|
35
|
-
# @raise [ArgumentError] If the input string is invalid
|
36
|
-
#
|
37
|
-
# @example Valid games turn string with uppercase first
|
38
|
-
# GamesTurn.parse("CHESS/ogi")
|
39
|
-
# # => ["CHESS", "ogi"]
|
40
|
-
#
|
41
|
-
# @example Valid games turn string with lowercase first
|
42
|
-
# GamesTurn.parse("chess/OGI")
|
43
|
-
# # => ["chess", "OGI"]
|
44
|
-
def self.parse(games_turn_str)
|
45
|
-
validate_input_type(games_turn_str)
|
46
|
-
|
47
|
-
match = VALID_GAMES_TURN_PATTERN.match(games_turn_str)
|
48
|
-
raise ::ArgumentError, ERRORS[:invalid_format] unless match
|
49
|
-
|
50
|
-
extract_game_identifiers(match)
|
51
|
-
end
|
52
|
-
|
53
|
-
# Validates that the input is a non-empty string
|
54
|
-
#
|
55
|
-
# @param str [String] Input string to validate
|
56
|
-
# @raise [ArgumentError] If input is not a string or is empty
|
57
|
-
# @return [void]
|
58
|
-
private_class_method def self.validate_input_type(str)
|
59
|
-
raise ::ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(::String)
|
60
|
-
raise ::ArgumentError, ERRORS[:empty_string] if str.empty?
|
61
|
-
end
|
62
|
-
|
63
|
-
# Extracts game identifiers from regexp match captures
|
64
|
-
#
|
65
|
-
# @param match [MatchData] Regexp match data with named captures
|
66
|
-
# @return [Array<String>] Array containing [active_player, inactive_player]
|
67
|
-
private_class_method def self.extract_game_identifiers(match)
|
68
|
-
captures = match.named_captures
|
69
|
-
|
70
|
-
if captures["uppercase_first"]
|
71
|
-
[captures["uppercase_first"], captures["lowercase_second"]]
|
72
|
-
else
|
73
|
-
[captures["lowercase_first"], captures["uppercase_second"]]
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|