feen 5.0.0.beta7 → 5.0.0.beta8

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.
@@ -4,12 +4,13 @@ module Feen
4
4
  module Dumper
5
5
  # Handles conversion of games turn data to FEEN notation string
6
6
  module GamesTurn
7
+ # Error messages for validation
7
8
  ERRORS = {
8
- type: "%s must be a String, got %s",
9
- empty: "%s cannot be empty",
10
- mixed: "%s has mixed case: %s",
11
- casing: "One variant must be uppercase and the other lowercase",
12
- chars: "Variant identifiers must contain only alphabetic characters (a-z, A-Z)"
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"
13
14
  }.freeze
14
15
 
15
16
  # Converts the active and inactive variant identifiers to a FEEN-formatted games turn string
@@ -17,6 +18,15 @@ module Feen
17
18
  # @param active_variant [String] Identifier for the player to move and their game variant
18
19
  # @param inactive_variant [String] Identifier for the opponent and their game variant
19
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", "SHOGI")
29
+ # # => ArgumentError: One variant must be uppercase and the other lowercase
20
30
  def self.dump(active_variant, inactive_variant)
21
31
  validate_variants(active_variant, inactive_variant)
22
32
  "#{active_variant}/#{inactive_variant}"
@@ -29,38 +39,31 @@ module Feen
29
39
  # @raise [ArgumentError] If the variant identifiers are invalid
30
40
  # @return [void]
31
41
  private_class_method def self.validate_variants(active, inactive)
32
- # Validate basic type and presence
42
+ # Validate basic type, presence and format
33
43
  [["Active variant", active], ["Inactive variant", inactive]].each do |name, variant|
34
- raise ArgumentError, format(ERRORS[:type], name, variant.class) unless variant.is_a?(String)
35
- raise ArgumentError, format(ERRORS[:empty], name) if variant.empty?
36
- raise ArgumentError, ERRORS[:chars] unless variant.match?(/\A[a-zA-Z]+\z/)
37
- end
44
+ # Type validation
45
+ raise ArgumentError, format(ERRORS[:invalid_type], name, variant.class) unless variant.is_a?(String)
38
46
 
39
- # Validate casing (one must be uppercase, one must be lowercase)
40
- active_uppercase = active == active.upcase && active != active.downcase
41
- inactive_uppercase = inactive == inactive.upcase && inactive != inactive.downcase
47
+ # Empty validation
48
+ raise ArgumentError, format(ERRORS[:empty_string], name) if variant.empty?
42
49
 
43
- # If both have the same casing (both uppercase or both lowercase), raise error
44
- raise ArgumentError, ERRORS[:casing] if active_uppercase == inactive_uppercase
50
+ # Character validation
51
+ raise ArgumentError, format(ERRORS[:invalid_chars], name) unless variant.match?(/\A[a-zA-Z]+\z/)
45
52
 
46
- # Check for mixed case (must be all uppercase or all lowercase)
47
- if active_uppercase && active != active.upcase
48
- raise ArgumentError, format(ERRORS[:mixed], "Active variant", active)
53
+ # Mixed case validation
54
+ unless variant == variant.upcase || variant == variant.downcase
55
+ raise ArgumentError, format(ERRORS[:mixed_case], name, variant)
56
+ end
49
57
  end
50
58
 
51
- if inactive_uppercase && inactive != inactive.upcase
52
- raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
53
- end
54
-
55
- if !active_uppercase && active != active.downcase
56
- raise ArgumentError, format(ERRORS[:mixed], "Active variant", active)
57
- end
59
+ # Casing difference validation
60
+ active_is_uppercase = active == active.upcase
61
+ inactive_is_uppercase = inactive == inactive.upcase
58
62
 
59
- if !inactive_uppercase && inactive != inactive.downcase
60
- raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
61
- end
63
+ # Both variants must have different casing
64
+ return unless active_is_uppercase == inactive_is_uppercase
62
65
 
63
- true
66
+ raise ArgumentError, ERRORS[:same_casing]
64
67
  end
65
68
  end
66
69
  end
@@ -2,69 +2,91 @@
2
2
 
3
3
  module Feen
4
4
  module Dumper
5
+ # Handles conversion of piece placement data to FEEN notation string
5
6
  module PiecePlacement
7
+ # Error messages
8
+ ERRORS = {
9
+ invalid_type: "Piece placement must be an Array, got %s",
10
+ inconsistent_shape: "Inconsistent dimension structure detected",
11
+ invalid_cell: "Invalid cell content: %s (must be String)"
12
+ }.freeze
13
+
6
14
  # Converts a piece placement structure to a FEEN-compliant string
7
15
  #
8
16
  # @param piece_placement [Array] Hierarchical array representing the board where:
9
17
  # - Empty squares are represented by empty strings ("")
10
- # - Pieces are represented by strings (e.g., "r", "R'", "+P")
18
+ # - Pieces are represented by strings (e.g., "r", "R", "+P", "K'")
11
19
  # - Dimensions are represented by nested arrays
12
20
  # @return [String] FEEN piece placement string
13
21
  # @raise [ArgumentError] If the piece placement structure is invalid
22
+ #
23
+ # @example 2D chess board
24
+ # PiecePlacement.dump([
25
+ # ["r", "n", "b", "q", "k", "b", "n", "r"],
26
+ # ["p", "p", "p", "p", "p", "p", "p", "p"],
27
+ # ["", "", "", "", "", "", "", ""],
28
+ # ["", "", "", "", "", "", "", ""],
29
+ # ["", "", "", "", "", "", "", ""],
30
+ # ["", "", "", "", "", "", "", ""],
31
+ # ["P", "P", "P", "P", "P", "P", "P", "P"],
32
+ # ["R", "N", "B", "Q", "K", "B", "N", "R"]
33
+ # ])
34
+ # # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
35
+ #
36
+ # @example 3D board
37
+ # PiecePlacement.dump([
38
+ # [["r", "n"], ["p", "p"]],
39
+ # [["", ""], ["P", "P"]],
40
+ # [["R", "N"], ["", ""]]
41
+ # ])
42
+ # # => "rn/pp//2/PP//RN/2"
14
43
  def self.dump(piece_placement)
15
- # Détecter la forme du tableau directement à partir de la structure
16
- detect_shape(piece_placement)
17
-
18
- # Formater directement la structure en chaîne FEEN
44
+ validate_input(piece_placement)
19
45
  format_placement(piece_placement)
20
46
  end
21
47
 
22
- # Detects the shape of the board based on the piece_placement structure
48
+ # Validates the input piece placement structure
23
49
  #
24
- # @param piece_placement [Array] Hierarchical array structure representing the board
25
- # @return [Array<Integer>] Array of dimension sizes
26
- # @raise [ArgumentError] If the piece_placement structure is invalid
27
- def self.detect_shape(piece_placement)
28
- return [] if piece_placement.empty?
29
-
30
- dimensions = []
31
- current = piece_placement
50
+ # @param piece_placement [Object] Structure to validate
51
+ # @raise [ArgumentError] If the structure is invalid
52
+ # @return [void]
53
+ private_class_method def self.validate_input(piece_placement)
54
+ raise ArgumentError, format(ERRORS[:invalid_type], piece_placement.class) unless piece_placement.is_a?(Array)
32
55
 
33
- # Traverse the structure to determine shape
34
- while current.is_a?(Array) && !current.empty?
35
- dimensions << current.size
36
-
37
- # Check if all elements at this level have the same structure
38
- validate_dimension_uniformity(current)
39
-
40
- # Check if we've reached the leaf level (array of strings)
41
- break if current.first.is_a?(String) ||
42
- (current.first.is_a?(Array) && current.first.empty?)
43
-
44
- current = current.first
45
- end
46
-
47
- dimensions
56
+ validate_structure_consistency(piece_placement)
48
57
  end
49
58
 
50
- # Validates that all elements in a dimension have the same structure
59
+ # Validates that the structure is consistent (all dimensions have same size)
51
60
  #
52
- # @param dimension [Array] Array of elements at a particular dimension level
53
- # @raise [ArgumentError] If elements have inconsistent structure
54
- def self.validate_dimension_uniformity(dimension)
55
- return if dimension.empty?
61
+ # @param structure [Array] Structure to validate
62
+ # @raise [ArgumentError] If structure is inconsistent
63
+ # @return [void]
64
+ private_class_method def self.validate_structure_consistency(structure)
65
+ return if structure.empty?
66
+
67
+ # Check if this is a rank (array of strings) or multi-dimensional
68
+ if structure.all?(String)
69
+ # This is a rank - validate each cell
70
+ structure.each_with_index do |cell, _index|
71
+ raise ArgumentError, format(ERRORS[:invalid_cell], cell.inspect) unless cell.is_a?(String)
72
+ end
73
+ elsif structure.all?(Array)
74
+ # This is multi-dimensional - check consistency and validate recursively
75
+ structure.each do |element|
76
+ # Recursively validate sub-structures
77
+ validate_structure_consistency(element)
78
+ end
79
+ else
80
+ # Mixed types - check for non-string elements in what should be a rank
81
+ non_string_elements = structure.reject { |element| element.is_a?(String) }
82
+ raise ArgumentError, ERRORS[:inconsistent_shape] unless non_string_elements.any?
56
83
 
57
- first_type = dimension.first.class
58
- first_size = dimension.first.is_a?(Array) ? dimension.first.size : nil
84
+ # If we have non-string elements, report the first one as invalid cell content
85
+ first_invalid = non_string_elements.first
86
+ raise ArgumentError, format(ERRORS[:invalid_cell], first_invalid.inspect)
59
87
 
60
- dimension.each do |element|
61
- unless element.class == first_type
62
- raise ArgumentError, "Inconsistent element types in dimension: #{first_type} vs #{element.class}"
63
- end
88
+ # This shouldn't happen, but keep the original error as fallback
64
89
 
65
- if element.is_a?(Array) && element.size != first_size
66
- raise ArgumentError, "Inconsistent dimension sizes: expected #{first_size}, got #{element.size}"
67
- end
68
90
  end
69
91
  end
70
92
 
@@ -72,28 +94,26 @@ module Feen
72
94
  #
73
95
  # @param placement [Array] Piece placement structure
74
96
  # @return [String] FEEN piece placement string
75
- def self.format_placement(placement)
76
- # For 1D arrays (ranks), format directly
77
- if !placement.is_a?(Array) ||
78
- (placement.is_a?(Array) && (placement.empty? || !placement.first.is_a?(Array)))
79
- return format_rank(placement)
80
- end
97
+ private_class_method def self.format_placement(placement)
98
+ return "" if placement.empty?
99
+
100
+ # Check if this is a rank (1D array of strings)
101
+ return format_rank(placement) if placement.all?(String)
81
102
 
82
- # For 2D+ arrays, format each sub-element and join with appropriate separator
103
+ # This is multi-dimensional - determine separator depth
83
104
  depth = calculate_depth(placement) - 1
84
105
  separator = "/" * depth
85
106
 
86
- # Important: Ne pas inverser le tableau - nous voulons maintenir l'ordre original
87
- elements = placement
88
- elements.map { |element| format_placement(element) }.join(separator)
107
+ # Format each sub-element and join
108
+ placement.map { |element| format_placement(element) }.join(separator)
89
109
  end
90
110
 
91
- # Formats a rank (1D array of cells)
111
+ # Formats a rank (1D array of cells) into FEEN notation
92
112
  #
93
- # @param rank [Array] 1D array of cells
113
+ # @param rank [Array<String>] 1D array of cells
94
114
  # @return [String] FEEN rank string
95
- def self.format_rank(rank)
96
- return "" if !rank.is_a?(Array) || rank.empty?
115
+ private_class_method def self.format_rank(rank)
116
+ return "" if rank.empty?
97
117
 
98
118
  result = ""
99
119
  empty_count = 0
@@ -102,9 +122,11 @@ module Feen
102
122
  if cell.empty?
103
123
  empty_count += 1
104
124
  else
105
- # Add accumulated empty squares
106
- result += empty_count.to_s if empty_count > 0
107
- empty_count = 0
125
+ # Add accumulated empty squares count
126
+ if empty_count.positive?
127
+ result += empty_count.to_s
128
+ empty_count = 0
129
+ end
108
130
 
109
131
  # Add the piece
110
132
  result += cell
@@ -112,16 +134,16 @@ module Feen
112
134
  end
113
135
 
114
136
  # Add any trailing empty squares
115
- result += empty_count.to_s if empty_count > 0
137
+ result += empty_count.to_s if empty_count.positive?
116
138
 
117
139
  result
118
140
  end
119
141
 
120
- # Calculates the depth of a nested structure
142
+ # Calculates the depth of a nested array structure
121
143
  #
122
144
  # @param structure [Array] Structure to analyze
123
145
  # @return [Integer] Depth of the structure
124
- def self.calculate_depth(structure)
146
+ private_class_method def self.calculate_depth(structure)
125
147
  return 0 unless structure.is_a?(Array) && !structure.empty?
126
148
 
127
149
  if structure.first.is_a?(Array)
@@ -1,30 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("pieces_in_hand", "errors")
4
-
5
3
  module Feen
6
4
  module Dumper
7
5
  # Handles conversion of pieces in hand data to FEEN notation string
8
6
  module PiecesInHand
7
+ # Error messages for validation
8
+ ERRORS = {
9
+ invalid_type: "Piece at index %d must be a String, got %s",
10
+ invalid_format: "Piece at index %d must be base form only (single letter): '%s'",
11
+ has_modifiers: "Piece at index %d cannot contain modifiers: '%s'. Pieces in hand must be base form only"
12
+ }.freeze
13
+
9
14
  # Converts an array of piece identifiers to a FEEN-formatted pieces in hand string
10
15
  #
11
- # @param piece_chars [Array<String>] Array of piece identifiers (e.g., ["P", "p", "B", "B", "p", "+P"])
16
+ # @param piece_chars [Array<String>] Array of piece identifiers in base form only (e.g., ["P", "p", "B", "B", "p"])
12
17
  # @return [String] FEEN-formatted pieces in hand string following the format:
13
18
  # - Groups pieces by case: uppercase first, then lowercase, separated by "/"
14
19
  # - Within each group, sorts by quantity (descending), then alphabetically (ascending)
15
20
  # - Uses count notation for quantities > 1 (e.g., "3P" instead of "PPP")
16
- # @raise [ArgumentError] If any piece identifier is invalid
17
- # @example
21
+ # @raise [ArgumentError] If any piece identifier is invalid or contains modifiers
22
+ #
23
+ # @example Valid pieces in hand
18
24
  # PiecesInHand.dump("P", "P", "P", "B", "B", "p", "p", "p", "p", "p")
19
25
  # # => "3P2B/5p"
20
26
  #
27
+ # @example Valid pieces in hand with mixed order
21
28
  # PiecesInHand.dump("p", "P", "B")
22
29
  # # => "BP/p"
23
30
  #
24
- # PiecesInHand.dump
31
+ # @example No pieces in hand
32
+ # PiecesInHand.dump()
25
33
  # # => "/"
34
+ #
35
+ # @example Invalid - modifiers not allowed
36
+ # PiecesInHand.dump("+P", "p")
37
+ # # => ArgumentError: Piece at index 0 cannot contain modifiers: '+P'
26
38
  def self.dump(*piece_chars)
27
- # Validate each piece character according to the FEEN specification
39
+ # Validate each piece character according to FEEN specification (base form only)
28
40
  validated_chars = validate_piece_chars(piece_chars)
29
41
 
30
42
  # Group pieces by case
@@ -43,34 +55,12 @@ module Feen
43
55
  # @param pieces [Array<String>] Array of validated piece identifiers
44
56
  # @return [Array<Array<String>, Array<String>>] Two arrays: [uppercase_pieces, lowercase_pieces]
45
57
  private_class_method def self.group_pieces_by_case(pieces)
46
- uppercase_pieces = pieces.select { |piece| piece_is_uppercase?(piece) }
47
- lowercase_pieces = pieces.select { |piece| piece_is_lowercase?(piece) }
58
+ uppercase_pieces = pieces.grep(/[A-Z]/)
59
+ lowercase_pieces = pieces.grep(/[a-z]/)
48
60
 
49
61
  [uppercase_pieces, lowercase_pieces]
50
62
  end
51
63
 
52
- # Determines if a piece belongs to the uppercase group
53
- # A piece is considered uppercase if its main letter is uppercase (ignoring prefixes/suffixes)
54
- #
55
- # @param piece [String] Piece identifier (e.g., "P", "+P", "P'", "+P'")
56
- # @return [Boolean] True if the piece's main letter is uppercase
57
- private_class_method def self.piece_is_uppercase?(piece)
58
- # Extract the main letter (skip prefixes like + or -)
59
- main_letter = piece.gsub(/\A[+-]/, "").gsub(/'\z/, "")
60
- main_letter.match?(/[A-Z]/)
61
- end
62
-
63
- # Determines if a piece belongs to the lowercase group
64
- # A piece is considered lowercase if its main letter is lowercase (ignoring prefixes/suffixes)
65
- #
66
- # @param piece [String] Piece identifier (e.g., "p", "+p", "p'", "+p'")
67
- # @return [Boolean] True if the piece's main letter is lowercase
68
- private_class_method def self.piece_is_lowercase?(piece)
69
- # Extract the main letter (skip prefixes like + or -)
70
- main_letter = piece.gsub(/\A[+-]/, "").gsub(/'\z/, "")
71
- main_letter.match?(/[a-z]/)
72
- end
73
-
74
64
  # Formats a group of pieces according to FEEN specification
75
65
  #
76
66
  # @param pieces [Array<String>] Array of pieces from the same case group
@@ -106,11 +96,11 @@ module Feen
106
96
  end.join
107
97
  end
108
98
 
109
- # Validates all piece characters according to FEEN specification
99
+ # Validates all piece characters according to FEEN specification (base form only)
110
100
  #
111
101
  # @param piece_chars [Array<Object>] Array of piece character candidates
112
102
  # @return [Array<String>] Array of validated piece characters
113
- # @raise [ArgumentError] If any piece character is invalid
103
+ # @raise [ArgumentError] If any piece character is invalid or contains modifiers
114
104
  private_class_method def self.validate_piece_chars(piece_chars)
115
105
  piece_chars.each_with_index.map do |char, index|
116
106
  validate_piece_char(char, index)
@@ -118,34 +108,22 @@ module Feen
118
108
  end
119
109
 
120
110
  # Validates a single piece character according to FEEN specification
121
- # Supports full PNN notation: [prefix]letter[suffix] where:
122
- # - prefix can be "+" or "-"
123
- # - letter must be a-z or A-Z
124
- # - suffix can be "'"
111
+ # For pieces in hand, only base form is allowed: single letter (a-z or A-Z)
112
+ # NO modifiers (+, -, ') are allowed in pieces in hand
125
113
  #
126
114
  # @param char [Object] Piece character candidate
127
115
  # @param index [Integer] Index of the character in the original array
128
116
  # @return [String] Validated piece character
129
- # @raise [ArgumentError] If the piece character is invalid
117
+ # @raise [ArgumentError] If the piece character is invalid or contains modifiers
130
118
  private_class_method def self.validate_piece_char(char, index)
131
119
  # Validate type
132
- unless char.is_a?(::String)
133
- raise ::ArgumentError, format(
134
- Errors[:invalid_type],
135
- index: index,
136
- type: char.class
137
- )
138
- end
120
+ raise ArgumentError, format(ERRORS[:invalid_type], index, char.class) unless char.is_a?(String)
139
121
 
140
- # Validate format using PNN pattern: [prefix]letter[suffix]
141
- # where prefix is +/-, letter is a-zA-Z, suffix is '
142
- unless char.match?(/\A[+-]?[a-zA-Z]'?\z/)
143
- raise ::ArgumentError, format(
144
- Errors[:invalid_format],
145
- index: index,
146
- value: char
147
- )
148
- end
122
+ # Check for forbidden modifiers first (clearer error message)
123
+ raise ArgumentError, format(ERRORS[:has_modifiers], index, char) if char.match?(/[+\-']/)
124
+
125
+ # Validate format: must be exactly one letter (base form only)
126
+ raise ArgumentError, format(ERRORS[:invalid_format], index, char) unless char.match?(/\A[a-zA-Z]\z/)
149
127
 
150
128
  char
151
129
  end
data/lib/feen/dumper.rb CHANGED
@@ -35,13 +35,13 @@ module Feen
35
35
  # pieces_in_hand: [],
36
36
  # games_turn: ["CHESS", "chess"]
37
37
  # )
38
- # # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
38
+ # # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
39
39
  #
40
40
  # @param piece_placement [Array] Board position data structure representing the spatial
41
41
  # distribution of pieces across the board, where each cell
42
42
  # is represented by a String (or empty string for empty cells)
43
43
  # @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board,
44
- # each represented as a single character string
44
+ # each represented as a single character string (base form only)
45
45
  # @param games_turn [Array<String>] A two-element array where the first element is the
46
46
  # active player's variant and the second is the inactive player's variant
47
47
  # @return [String] Complete FEEN string representation compliant with the specification
@@ -1,12 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("games_turn", "valid_games_turn_pattern")
4
- require_relative File.join("games_turn", "errors")
5
-
6
3
  module Feen
7
4
  module Parser
8
5
  # Handles parsing of the games turn section of a FEEN string
9
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
+
10
31
  # Parses the games turn section of a FEEN string
11
32
  #
12
33
  # @param games_turn_str [String] FEEN games turn string
@@ -23,10 +44,10 @@ module Feen
23
44
  def self.parse(games_turn_str)
24
45
  validate_input_type(games_turn_str)
25
46
 
26
- match = ValidGamesTurnPattern.match(games_turn_str)
27
- raise ::ArgumentError, Errors[:invalid_format] unless match
47
+ match = VALID_GAMES_TURN_PATTERN.match(games_turn_str)
48
+ raise ::ArgumentError, ERRORS[:invalid_format] unless match
28
49
 
29
- extract_game_identifiers(**match.named_captures.transform_keys(&:to_sym))
50
+ extract_game_identifiers(match)
30
51
  end
31
52
 
32
53
  # Validates that the input is a non-empty string
@@ -35,22 +56,21 @@ module Feen
35
56
  # @raise [ArgumentError] If input is not a string or is empty
36
57
  # @return [void]
37
58
  private_class_method def self.validate_input_type(str)
38
- raise ::ArgumentError, format(Errors[:invalid_type], str.class) unless str.is_a?(::String)
39
- raise ::ArgumentError, Errors[:empty_string] if str.empty?
59
+ raise ::ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(::String)
60
+ raise ::ArgumentError, ERRORS[:empty_string] if str.empty?
40
61
  end
41
62
 
42
63
  # Extracts game identifiers from regexp match captures
43
64
  #
44
- # @param uppercase_first [String, nil] Uppercase identifier if it comes first
45
- # @param lowercase_second [String, nil] Lowercase identifier if it comes second
46
- # @param lowercase_first [String, nil] Lowercase identifier if it comes first
47
- # @param uppercase_second [String, nil] Uppercase identifier if it comes second
65
+ # @param match [MatchData] Regexp match data with named captures
48
66
  # @return [Array<String>] Array containing [active_player, inactive_player]
49
- private_class_method def self.extract_game_identifiers(uppercase_first: nil, lowercase_second: nil, lowercase_first: nil, uppercase_second: nil)
50
- if uppercase_first
51
- [uppercase_first, lowercase_second]
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"]]
52
72
  else
53
- [lowercase_first, uppercase_second]
73
+ [captures["lowercase_first"], captures["uppercase_second"]]
54
74
  end
55
75
  end
56
76
  end