feen 5.0.0.beta2 → 5.0.0.beta3

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.
@@ -1,135 +1,57 @@
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
+
3
6
  module Feen
4
7
  module Parser
5
8
  # Handles parsing of the games turn section of a FEEN string
6
9
  module GamesTurn
7
- ERRORS = {
8
- invalid_type: "Games turn must be a string, got %s",
9
- empty_string: "Games turn string cannot be empty",
10
- separator: "Games turn must contain exactly one '/' separator",
11
- mixed_casing: "%s game has mixed case: %s",
12
- casing_requirement: "One game must use uppercase letters and the other lowercase letters",
13
- invalid_chars: "Invalid characters in %s game identifier: %s",
14
- empty_identifier: "%s game identifier cannot be empty"
15
- }.freeze
16
-
17
10
  # Parses the games turn section of a FEEN string
18
11
  #
19
12
  # @param games_turn_str [String] FEEN games turn string
20
- # @return [Hash] Hash containing game turn information
13
+ # @return [Array<String>] Array containing [active_player, inactive_player]
21
14
  # @raise [ArgumentError] If the input string is invalid
22
- def self.parse(games_turn_str)
23
- validate_games_turn_string(games_turn_str)
24
-
25
- # Split by the forward slash
26
- parts = games_turn_str.split("/")
27
- active_player = parts[0]
28
- inactive_player = parts[1]
29
-
30
- # Determine casing
31
- active_uppercase = contains_uppercase?(active_player)
32
-
33
- # Build result hash
34
- {
35
- active_player: active_player,
36
- inactive_player: inactive_player,
37
- uppercase_game: active_uppercase ? active_player : inactive_player,
38
- lowercase_game: active_uppercase ? inactive_player : active_player,
39
- active_player_casing: active_uppercase ? :uppercase : :lowercase
40
- }
41
- end
42
-
43
- # Validates the games turn string for syntax and semantics
44
15
  #
45
- # @param str [String] FEEN games turn string
46
- # @raise [ArgumentError] If the string is invalid
47
- # @return [void]
48
- def self.validate_games_turn_string(str)
49
- validate_basic_structure(str)
50
-
51
- parts = str.split("/")
52
- validate_game_identifiers(parts)
53
- validate_casing_requirements(parts)
54
- end
55
-
56
- # Validates the basic structure of the games turn string
16
+ # @example Valid games turn string with uppercase first
17
+ # GamesTurn.parse("CHESS/shogi")
18
+ # # => ["CHESS", "shogi"]
57
19
  #
58
- # @param str [String] FEEN games turn string
59
- # @raise [ArgumentError] If the structure is invalid
60
- # @return [void]
61
- private_class_method def self.validate_basic_structure(str)
62
- raise ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(String)
63
-
64
- raise ArgumentError, ERRORS[:empty_string] if str.empty?
20
+ # @example Valid games turn string with lowercase first
21
+ # GamesTurn.parse("chess/SHOGI")
22
+ # # => ["chess", "SHOGI"]
23
+ def self.parse(games_turn_str)
24
+ validate_input_type(games_turn_str)
65
25
 
66
- # Check for exactly one separator '/'
67
- return if str.count("/") == 1
26
+ match = ValidGamesTurnPattern.match(games_turn_str)
27
+ raise ::ArgumentError, Errors[:invalid_format] unless match
68
28
 
69
- raise ArgumentError, ERRORS[:separator]
29
+ extract_game_identifiers(**match.named_captures.transform_keys(&:to_sym))
70
30
  end
71
31
 
72
- # Validates the individual game identifiers
32
+ # Validates that the input is a non-empty string
73
33
  #
74
- # @param parts [Array<String>] Split game identifiers
75
- # @raise [ArgumentError] If any identifier is invalid
34
+ # @param str [String] Input string to validate
35
+ # @raise [ArgumentError] If input is not a string or is empty
76
36
  # @return [void]
77
- private_class_method def self.validate_game_identifiers(parts)
78
- parts.each_with_index do |game_id, idx|
79
- position = idx == 0 ? "active" : "inactive"
80
-
81
- raise ArgumentError, format(ERRORS[:empty_identifier], position.capitalize) if game_id.nil? || game_id.empty?
82
-
83
- unless game_id.match?(/\A[a-zA-Z]+\z/)
84
- invalid_chars = game_id.scan(/[^a-zA-Z]/).uniq.join(", ")
85
- raise ArgumentError, format(ERRORS[:invalid_chars], position, invalid_chars)
86
- end
87
- end
37
+ 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?
88
40
  end
89
41
 
90
- # Validates the casing requirements (one uppercase, one lowercase)
42
+ # Extracts game identifiers from regexp match captures
91
43
  #
92
- # @param parts [Array<String>] Split game identifiers
93
- # @raise [ArgumentError] If casing requirements aren't met
94
- # @return [void]
95
- private_class_method def self.validate_casing_requirements(parts)
96
- active_uppercase = contains_uppercase?(parts[0])
97
- inactive_uppercase = contains_uppercase?(parts[1])
98
-
99
- raise ArgumentError, ERRORS[:casing_requirement] if active_uppercase == inactive_uppercase
100
-
101
- # Verify consistent casing in each identifier
102
- if active_uppercase && contains_lowercase?(parts[0])
103
- raise ArgumentError, format(ERRORS[:mixed_casing], "Active", parts[0])
104
- end
105
-
106
- if inactive_uppercase && contains_lowercase?(parts[1])
107
- raise ArgumentError, format(ERRORS[:mixed_casing], "Inactive", parts[1])
108
- end
109
-
110
- if !active_uppercase && contains_uppercase?(parts[0])
111
- raise ArgumentError, format(ERRORS[:mixed_casing], "Active", parts[0])
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
48
+ # @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]
52
+ else
53
+ [lowercase_first, uppercase_second]
112
54
  end
113
-
114
- return unless !inactive_uppercase && contains_uppercase?(parts[1])
115
-
116
- raise ArgumentError, format(ERRORS[:mixed_casing], "Inactive", parts[1])
117
- end
118
-
119
- # Checks if a string contains any uppercase letters
120
- #
121
- # @param str [String] String to check
122
- # @return [Boolean] True if the string contains uppercase letters
123
- def self.contains_uppercase?(str)
124
- str.match?(/[A-Z]/)
125
- end
126
-
127
- # Checks if a string contains any lowercase letters
128
- #
129
- # @param str [String] String to check
130
- # @return [Boolean] True if the string contains lowercase letters
131
- def self.contains_lowercase?(str)
132
- str.match?(/[a-z]/)
133
55
  end
134
56
  end
135
57
  end