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.
- checksums.yaml +4 -4
- data/README.md +135 -87
- data/lib/feen/dumper/games_turn.rb +40 -65
- data/lib/feen/dumper/piece_placement.rb +102 -89
- data/lib/feen/dumper/pieces_in_hand/errors.rb +12 -0
- data/lib/feen/dumper/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/dumper/pieces_in_hand.rb +57 -41
- data/lib/feen/dumper.rb +63 -32
- data/lib/feen/parser/games_turn/errors.rb +14 -0
- data/lib/feen/parser/games_turn/valid_games_turn_pattern.rb +24 -0
- data/lib/feen/parser/games_turn.rb +32 -110
- data/lib/feen/parser/piece_placement.rb +490 -77
- data/lib/feen/parser/pieces_in_hand/errors.rb +15 -0
- data/lib/feen/parser/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/parser/pieces_in_hand/piece_count_pattern.rb +13 -0
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +29 -0
- data/lib/feen/parser/pieces_in_hand.rb +90 -45
- data/lib/feen/parser.rb +67 -26
- data/lib/feen.rb +95 -76
- metadata +21 -7
- data/lib/feen/converter/from_fen.rb +0 -170
- data/lib/feen/converter/to_fen.rb +0 -153
- data/lib/feen/converter.rb +0 -16
- data/lib/feen/sanitizer.rb +0 -119
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de87bfc9b071e9f1ff14b6f7378ba0a6f9f78cdb9a9bfe32ca00f2494a1be2cd
|
4
|
+
data.tar.gz: fd4d19e8a3b7fb845f081b127a859ff0791c8c11c05e1efb4846152044f67c18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9fe95297b8577a3bf171a865deca0dc9c2eed91a7d039c04f97077d53d3fa912ca553f28f448f0dd987087926d3ca5225624d7f2d8d08a9275a16a11efe26e0
|
7
|
+
data.tar.gz: b82b7e00f5ece6f5e5859750226be3c74db23e694c8e53def8c8c656fe2fb3ecba7f0f4166cf3d9a4f6c7b4ab7af3cf6544da87a2684e438a389d96e04b3dc75
|
data/README.md
CHANGED
@@ -1,28 +1,28 @@
|
|
1
1
|
# Feen.rb
|
2
2
|
|
3
|
-
[](https://github.com/sashite/feen.rb/
|
3
|
+
[](https://github.com/sashite/feen.rb/tags)
|
4
4
|
[](https://rubydoc.info/github/sashite/feen.rb/main)
|
5
|
-
|
6
|
-
[](https://github.com/sashite/feen.rb/actions?query=workflow%3Arubocop+branch%3Amain)
|
5
|
+

|
7
6
|
[](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
|
8
7
|
|
9
|
-
> **FEEN** (Forsyth–Edwards
|
8
|
+
> **FEEN** (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
|
10
9
|
|
11
10
|
## What is FEEN?
|
12
11
|
|
13
|
-
FEEN (Forsyth–Edwards
|
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.
|
14
13
|
|
15
14
|
This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/), providing a Ruby interface for:
|
16
|
-
-
|
17
|
-
-
|
18
|
-
-
|
19
|
-
-
|
15
|
+
- Representing positions from various games without knowledge of specific rules
|
16
|
+
- Supporting boards of arbitrary dimensions
|
17
|
+
- Encoding pieces in hand (as used in Shogi)
|
18
|
+
- Facilitating serialization and deserialization of positions
|
19
|
+
- Ensuring canonical representation for consistent data handling
|
20
20
|
|
21
21
|
## Installation
|
22
22
|
|
23
23
|
```ruby
|
24
24
|
# In your Gemfile
|
25
|
-
gem "feen", ">= 5.0.0.
|
25
|
+
gem "feen", ">= 5.0.0.beta4"
|
26
26
|
```
|
27
27
|
|
28
28
|
Or install manually:
|
@@ -36,7 +36,7 @@ gem install feen --pre
|
|
36
36
|
A FEEN record consists of three space-separated fields:
|
37
37
|
|
38
38
|
```
|
39
|
-
<PIECE-PLACEMENT> <
|
39
|
+
<PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
|
40
40
|
```
|
41
41
|
|
42
42
|
## Basic Usage
|
@@ -48,81 +48,136 @@ Convert a FEEN string into a structured Ruby object:
|
|
48
48
|
```ruby
|
49
49
|
require "feen"
|
50
50
|
|
51
|
-
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess
|
51
|
+
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
52
52
|
position = Feen.parse(feen_string)
|
53
53
|
|
54
|
-
# Result is a hash
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
54
|
+
# Result is a hash:
|
55
|
+
# {
|
56
|
+
# piece_placement: [
|
57
|
+
# ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
58
|
+
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
59
|
+
# ["", "", "", "", "", "", "", ""],
|
60
|
+
# ["", "", "", "", "", "", "", ""],
|
61
|
+
# ["", "", "", "", "", "", "", ""],
|
62
|
+
# ["", "", "", "", "", "", "", ""],
|
63
|
+
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
64
|
+
# ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
65
|
+
# ],
|
66
|
+
# pieces_in_hand: [],
|
67
|
+
# games_turn: ["CHESS", "chess"]
|
68
|
+
# }
|
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
|
58
85
|
```
|
59
86
|
|
60
87
|
### Creating FEEN Strings
|
61
88
|
|
62
|
-
Convert
|
89
|
+
Convert position components to a FEEN string using named arguments:
|
63
90
|
|
64
91
|
```ruby
|
65
92
|
require "feen"
|
66
93
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
94
|
+
# Representation of a chess board in initial position
|
95
|
+
piece_placement = [
|
96
|
+
["r", "n", "b", "q", "k=", "b", "n", "r"],
|
97
|
+
["p", "p", "p", "p", "p", "p", "p", "p"],
|
98
|
+
["", "", "", "", "", "", "", ""],
|
99
|
+
["", "", "", "", "", "", "", ""],
|
100
|
+
["", "", "", "", "", "", "", ""],
|
101
|
+
["", "", "", "", "", "", "", ""],
|
102
|
+
["P", "P", "P", "P", "P", "P", "P", "P"],
|
103
|
+
["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
104
|
+
]
|
105
|
+
|
106
|
+
result = Feen.dump(
|
107
|
+
piece_placement: piece_placement,
|
108
|
+
games_turn: %w[CHESS chess],
|
82
109
|
pieces_in_hand: []
|
83
|
-
|
84
|
-
|
85
|
-
Feen.dump(position)
|
86
|
-
# => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -"
|
110
|
+
)
|
111
|
+
# => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
87
112
|
```
|
88
113
|
|
89
114
|
### Validation
|
90
115
|
|
91
|
-
Check if a string is valid FEEN notation:
|
116
|
+
Check if a string is valid FEEN notation and in canonical form:
|
92
117
|
|
93
118
|
```ruby
|
94
119
|
require "feen"
|
95
120
|
|
96
|
-
|
121
|
+
# Canonical form
|
122
|
+
Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi")
|
97
123
|
# => true
|
98
124
|
|
125
|
+
# Invalid syntax
|
99
126
|
Feen.valid?("invalid feen string")
|
100
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
|
101
132
|
```
|
102
133
|
|
103
|
-
|
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
|
104
137
|
|
105
|
-
|
138
|
+
## Game Examples
|
139
|
+
|
140
|
+
As FEEN is rule-agnostic, it can represent positions from various board games. Here are some examples:
|
141
|
+
|
142
|
+
### International Chess
|
106
143
|
|
107
144
|
```ruby
|
108
|
-
|
145
|
+
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
146
|
+
```
|
147
|
+
|
148
|
+
In this initial chess position:
|
149
|
+
- The `=` suffixes on kings indicate castling rights on both sides (though FEEN doesn't define this semantics)
|
150
|
+
- The third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move
|
109
151
|
|
110
|
-
|
111
|
-
|
112
|
-
|
152
|
+
### Shogi (Japanese Chess)
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi"
|
113
156
|
```
|
114
157
|
|
115
|
-
|
158
|
+
In this shogi position:
|
159
|
+
- The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
|
160
|
+
- The notation allows for pieces in hand, indicated in the second field
|
161
|
+
- `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
|
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
|
163
|
+
|
164
|
+
### Makruk (Thai Chess)
|
116
165
|
|
117
166
|
```ruby
|
118
|
-
|
167
|
+
feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBQKBNR - MAKRUK/makruk"
|
168
|
+
```
|
169
|
+
|
170
|
+
This initial Makruk position is easily represented in FEEN without needing to know the specific rules of the game.
|
119
171
|
|
120
|
-
|
121
|
-
|
122
|
-
|
172
|
+
### Xiangqi (Chinese Chess)
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
|
123
176
|
```
|
124
177
|
|
125
|
-
|
178
|
+
In this Xiangqi position:
|
179
|
+
- The representation uses single letters for the different pieces
|
180
|
+
- The format naturally adapts to the presence of a "river" (empty space in the middle)
|
126
181
|
|
127
182
|
## Advanced Features
|
128
183
|
|
@@ -134,61 +189,54 @@ FEEN supports arbitrary-dimensional board configurations:
|
|
134
189
|
require "feen"
|
135
190
|
|
136
191
|
# 3D board
|
137
|
-
|
138
|
-
|
139
|
-
[
|
140
|
-
|
141
|
-
[{ id: "q" }, { id: "k" }, { id: "p" }]
|
142
|
-
],
|
143
|
-
[
|
144
|
-
[{ id: "P" }, { id: "R" }, nil],
|
145
|
-
[nil, { id: "K" }, { id: "Q" }]
|
146
|
-
]
|
192
|
+
piece_placement = [
|
193
|
+
[
|
194
|
+
%w[r n b],
|
195
|
+
%w[q k p]
|
147
196
|
],
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
197
|
+
[
|
198
|
+
["P", "R", ""],
|
199
|
+
["", "K", "Q"]
|
200
|
+
]
|
201
|
+
]
|
202
|
+
|
203
|
+
result = Feen.dump(
|
204
|
+
piece_placement: piece_placement,
|
205
|
+
games_turn: %w[FOO bar],
|
152
206
|
pieces_in_hand: []
|
153
|
-
|
154
|
-
|
155
|
-
Feen.dump(position)
|
156
|
-
# => "rnb/qkp//PR1/1KQ CHESS/chess -"
|
207
|
+
)
|
208
|
+
# => "rnb/qkp//PR1/1KQ - FOO/bar"
|
157
209
|
```
|
158
210
|
|
159
211
|
### Piece Modifiers
|
160
212
|
|
161
|
-
FEEN supports prefixes and suffixes for pieces:
|
213
|
+
FEEN supports prefixes and suffixes for pieces to denote various states or capabilities:
|
162
214
|
|
163
|
-
- Prefix
|
164
|
-
-
|
165
|
-
- Suffix `<`: Left-side constraint (e.g., `K<` for queenside castling only)
|
166
|
-
- Suffix `>`: Right-side constraint (e.g., `K>` for kingside castling only)
|
215
|
+
- **Prefix `+`**: May indicate promotion or special state
|
216
|
+
- Example in shogi: `+P` may represent a promoted pawn
|
167
217
|
|
168
|
-
|
218
|
+
- **Suffix `=`**: May indicate dual-option status
|
219
|
+
- Example in chess: `K=` may represent a king eligible for both kingside and queenside castling
|
169
220
|
|
170
|
-
|
221
|
+
- **Suffix `<`**: May indicate left-side constraint
|
222
|
+
- Example in chess: `K<` may represent a king eligible for queenside castling only
|
223
|
+
- Example in chess: `P<` may represent a pawn that may be captured _en passant_ from the left
|
171
224
|
|
172
|
-
|
173
|
-
|
225
|
+
- **Suffix `>`**: May indicate right-side constraint
|
226
|
+
- Example in chess: `K>` may represent a king eligible for kingside castling only
|
227
|
+
- Example in chess: `P>` may represent a pawn that may be captured en passant from the right
|
174
228
|
|
175
|
-
|
176
|
-
fen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQ1BNR w KQkq - 0 1"
|
177
|
-
cleaned_fen = Feen::Sanitizer.clean_fen(fen_string)
|
178
|
-
# => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQ1BNR w kq - 0 1"
|
179
|
-
```
|
229
|
+
These modifiers have no defined semantics in the FEEN specification itself but provide a flexible framework for representing piece-specific conditions while maintaining FEEN's rule-agnostic nature.
|
180
230
|
|
181
231
|
## Documentation
|
182
232
|
|
183
|
-
- [Official FEEN Specification
|
233
|
+
- [Official FEEN Specification](https://sashite.dev/documents/feen/1.0.0/)
|
184
234
|
- [API Documentation](https://rubydoc.info/github/sashite/feen.rb/main)
|
185
235
|
|
186
236
|
## License
|
187
237
|
|
188
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
238
|
+
The [gem](https://rubygems.org/gems/feen) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
189
239
|
|
190
240
|
## About Sashité
|
191
241
|
|
192
|
-
This
|
193
|
-
|
194
|
-
With some [lines of code](https://github.com/sashite/), let's share the beauty of Chinese, Japanese and Western cultures through the game of chess!
|
242
|
+
This project is maintained by [Sashité](https://sashite.com/) - a project dedicated to promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
@@ -2,90 +2,65 @@
|
|
2
2
|
|
3
3
|
module Feen
|
4
4
|
module Dumper
|
5
|
-
# Handles conversion of games turn data
|
5
|
+
# Handles conversion of games turn data to FEEN notation string
|
6
6
|
module GamesTurn
|
7
7
|
ERRORS = {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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)"
|
13
13
|
}.freeze
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
# Converts the internal games turn representation to a FEEN string
|
15
|
+
# Converts the active and inactive variant identifiers to a FEEN-formatted games turn string
|
18
16
|
#
|
19
|
-
# @param
|
17
|
+
# @param active_variant [String] Identifier for the player to move and their game variant
|
18
|
+
# @param inactive_variant [String] Identifier for the opponent and their game variant
|
20
19
|
# @return [String] FEEN-formatted games turn string
|
21
|
-
def self.dump(
|
22
|
-
|
23
|
-
|
24
|
-
# Format is <active_player>/<inactive_player>
|
25
|
-
"#{games_turn[:active_player]}/#{games_turn[:inactive_player]}"
|
26
|
-
end
|
27
|
-
|
28
|
-
# Validates the games turn data structure
|
29
|
-
#
|
30
|
-
# @param games_turn [Hash] The games turn data to validate
|
31
|
-
# @raise [ArgumentError] If the games turn data is invalid
|
32
|
-
# @return [Boolean] true if the validation passes
|
33
|
-
def self.validate_games_turn(games_turn)
|
34
|
-
validate_structure(games_turn)
|
35
|
-
validate_casing(games_turn)
|
36
|
-
validate_character_set(games_turn)
|
37
|
-
|
38
|
-
true
|
20
|
+
def self.dump(active_variant, inactive_variant)
|
21
|
+
validate_variants(active_variant, inactive_variant)
|
22
|
+
"#{active_variant}/#{inactive_variant}"
|
39
23
|
end
|
40
24
|
|
41
|
-
# Validates the
|
25
|
+
# Validates the game variant identifiers
|
42
26
|
#
|
43
|
-
# @param
|
44
|
-
# @
|
27
|
+
# @param active [String] The active player's variant identifier
|
28
|
+
# @param inactive [String] The inactive player's variant identifier
|
29
|
+
# @raise [ArgumentError] If the variant identifiers are invalid
|
45
30
|
# @return [void]
|
46
|
-
private_class_method def self.
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
raise ArgumentError, format(ERRORS[:empty_string], key) if games_turn[key].empty?
|
31
|
+
private_class_method def self.validate_variants(active, inactive)
|
32
|
+
# Validate basic type and presence
|
33
|
+
[["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/)
|
55
37
|
end
|
56
|
-
end
|
57
38
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# @raise [ArgumentError] If the casing requirement is not met
|
62
|
-
# @return [void]
|
63
|
-
private_class_method def self.validate_casing(games_turn)
|
64
|
-
active_has_uppercase = games_turn[:active_player].match?(/[A-Z]/)
|
65
|
-
inactive_has_uppercase = games_turn[:inactive_player].match?(/[A-Z]/)
|
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
|
66
42
|
|
67
|
-
#
|
68
|
-
raise ArgumentError, ERRORS[:
|
43
|
+
# If both have the same casing (both uppercase or both lowercase), raise error
|
44
|
+
raise ArgumentError, ERRORS[:casing] if active_uppercase == inactive_uppercase
|
69
45
|
|
70
|
-
# Check
|
71
|
-
if
|
72
|
-
raise ArgumentError, "Active
|
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)
|
73
49
|
end
|
74
50
|
|
75
|
-
|
51
|
+
if inactive_uppercase && inactive != inactive.upcase
|
52
|
+
raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
|
53
|
+
end
|
76
54
|
|
77
|
-
|
78
|
-
|
55
|
+
if !active_uppercase && active != active.downcase
|
56
|
+
raise ArgumentError, format(ERRORS[:mixed], "Active variant", active)
|
57
|
+
end
|
79
58
|
|
80
|
-
|
81
|
-
|
82
|
-
# @param games_turn [Hash] The games turn data to validate
|
83
|
-
# @raise [ArgumentError] If invalid characters are present
|
84
|
-
# @return [void]
|
85
|
-
private_class_method def self.validate_character_set(games_turn)
|
86
|
-
REQUIRED_KEYS.each do |key|
|
87
|
-
raise ArgumentError, ERRORS[:invalid_chars] unless games_turn[key].match?(/\A[a-zA-Z]+\z/)
|
59
|
+
if !inactive_uppercase && inactive != inactive.downcase
|
60
|
+
raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
|
88
61
|
end
|
62
|
+
|
63
|
+
true
|
89
64
|
end
|
90
65
|
end
|
91
66
|
end
|