sashite-epin 2.0.0 → 2.2.0

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.
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Epin
5
+ # Constants for EPIN (Extended Piece Identifier Notation).
6
+ #
7
+ # EPIN extends PIN with a single derivation marker.
8
+ # PIN constants (VALID_TYPES, VALID_SIDES, VALID_STATES, etc.)
9
+ # are accessed through the sashite-pin dependency.
10
+ module Constants
11
+ # Derivation marker suffix.
12
+ DERIVATION_SUFFIX = "'"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Epin
5
+ module Errors
6
+ class Argument < ::ArgumentError
7
+ # Centralized error messages for EPIN parsing and validation.
8
+ #
9
+ # PIN-related errors (empty input, must contain exactly one letter, etc.)
10
+ # are propagated from the sashite-pin dependency.
11
+ #
12
+ # @example
13
+ # raise Errors::Argument, Messages::INVALID_DERIVATION_MARKER
14
+ module Messages
15
+ # Parsing error
16
+ INVALID_DERIVATION_MARKER = "invalid derivation marker"
17
+
18
+ # Validation errors (constructor)
19
+ INVALID_PIN = "pin must be a Sashite::Pin::Identifier"
20
+ INVALID_DERIVED = "derived must be true or false"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "argument/messages"
4
+
5
+ module Sashite
6
+ module Epin
7
+ module Errors
8
+ # Error raised when EPIN parsing or validation fails.
9
+ #
10
+ # @example
11
+ # raise Argument, Argument::Messages::INVALID_DERIVATION_MARKER
12
+ class Argument < ::ArgumentError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors/argument"
@@ -1,293 +1,200 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sashite/pin"
3
+ require_relative "constants"
4
+ require_relative "errors"
4
5
 
5
6
  module Sashite
6
7
  module Epin
7
- # Represents an identifier in EPIN (Extended Piece Identifier Notation) format.
8
+ # Represents a parsed EPIN (Extended Piece Identifier Notation) identifier.
8
9
  #
9
- # EPIN extends PIN by adding a derivation marker to track piece style in cross-style games.
10
- # An EPIN identifier is simply a PIN identifier plus a boolean flag indicating whether
11
- # the piece uses its own side's native style (native) or the opponent's style (derived).
10
+ # An Identifier combines a PIN component with a derivation status:
11
+ # - PIN: encodes abbr, side, state, and terminal status
12
+ # - Derived: indicates whether the piece uses native or derived style
12
13
  #
13
- # ## Pure Composition Design
14
+ # Instances are immutable (frozen after creation).
14
15
  #
15
- # EPIN doesn't reimplement PIN features - it's pure composition:
16
- # - All piece attributes (name, side, state, terminal) come from the PIN component
17
- # - EPIN adds only style derivation tracking (native vs derived)
18
- # - Zero code duplication
19
- #
20
- # ## Minimal API
21
- #
22
- # Core methods (6 total):
23
- # 1. new(pin, derived: false) - create from PIN component
24
- # 2. pin - get PIN component
25
- # 3. derived? - check derivation status
26
- # 4. to_s - serialize
27
- # 5. with_pin(new_pin) - replace PIN component
28
- # 6. with_derived(boolean) - change derivation status
29
- #
30
- # Everything else uses the PIN component API directly.
31
- #
32
- # All instances are immutable - transformation methods return new instances.
33
- #
34
- # @example Basic usage
35
- # # Create from PIN component
16
+ # @example Creating identifiers
36
17
  # pin = Sashite::Pin.parse("K^")
37
- # epin = Sashite::Epin::Identifier.new(pin, derived: false)
38
- # epin.to_s # => "K^" (native)
39
- #
40
- # # Mark as derived
41
- # derived = epin.mark_derived
42
- # derived.to_s # => "K^'" (derived from opponent's style)
18
+ # epin = Identifier.new(pin)
19
+ # epin = Identifier.new(pin, derived: true)
43
20
  #
44
- # @example Accessing attributes via PIN component
45
- # epin = Sashite::Epin.parse("+R^'")
46
- # epin.pin.type # => :R (Piece Name)
47
- # epin.pin.side # => :first (Piece Side)
48
- # epin.pin.state # => :enhanced (Piece State)
49
- # epin.pin.terminal? # => true (Terminal Status)
50
- # epin.derived? # => true (Piece Style)
21
+ # @example String conversion
22
+ # Identifier.new(pin).to_s # => "K^"
23
+ # Identifier.new(pin, derived: true).to_s # => "K^'"
51
24
  #
52
- # @example Transformations
53
- # epin = Sashite::Epin.parse("K^")
54
- #
55
- # # Transform PIN component
56
- # epin.with_pin(epin.pin.with_type(:Q)) # => "Q^"
57
- #
58
- # # Transform derivation
59
- # epin.mark_derived # => "K^'"
60
- # epin.with_derived(true) # => "K^'"
61
- #
62
- # @see https://sashite.dev/specs/epin/1.0.0/ EPIN Specification v1.0.0
25
+ # @see https://sashite.dev/specs/epin/1.0.0/
63
26
  class Identifier
64
- # EPIN validation pattern matching the specification
65
- # Grammar: <epin> ::= <pin> | <pin> "'"
66
- EPIN_PATTERN = /\A[-+]?[A-Za-z]\^?'?\z/
67
-
68
- # Derivation marker character
69
- DERIVATION_MARKER = "'"
70
-
71
- # Error messages
72
- ERROR_INVALID_EPIN = "Invalid EPIN string: %s"
73
- ERROR_INVALID_PIN = "PIN component must be a Pin::Identifier, got: %s"
74
- ERROR_MULTIPLE_MARKERS = "EPIN string cannot have multiple derivation markers: %s"
75
-
76
- # @return [Pin::Identifier] the PIN component
27
+ # @return [Sashite::Pin::Identifier] PIN component
77
28
  attr_reader :pin
78
29
 
79
- # @return [Boolean] whether the piece uses derived style (opponent's native style)
80
- def derived?
81
- @derived
82
- end
83
-
84
- # Create a new EPIN identifier from PIN component and derivation flag
30
+ # Creates a new Identifier instance.
85
31
  #
86
- # @param pin [Pin::Identifier] the PIN component
87
- # @param derived [Boolean] whether the piece uses derived style (default: false)
88
- # @raise [ArgumentError] if pin is not a Pin::Identifier
32
+ # @param pin [Sashite::Pin::Identifier] PIN component
33
+ # @param derived [Boolean] Derived status
34
+ # @return [Identifier] A new frozen Identifier instance
35
+ # @raise [Errors::Argument] If any attribute is invalid
89
36
  #
90
- # @example Create EPIN identifiers
37
+ # @example
91
38
  # pin = Sashite::Pin.parse("K^")
92
- # native = Sashite::Epin::Identifier.new(pin, derived: false) # => "K^"
93
- # derived = Sashite::Epin::Identifier.new(pin, derived: true) # => "K^'"
39
+ # Identifier.new(pin)
40
+ # Identifier.new(pin, derived: true)
94
41
  def initialize(pin, derived: false)
95
- raise ::ArgumentError, format(ERROR_INVALID_PIN, pin.class) unless pin.is_a?(Pin::Identifier)
42
+ validate_pin!(pin)
43
+ validate_derived!(derived)
96
44
 
97
45
  @pin = pin
98
- @derived = !!derived
46
+ @derived = derived
99
47
 
100
48
  freeze
101
49
  end
102
50
 
103
- # Parse an EPIN string into an Identifier object
51
+ # Returns the derived status.
104
52
  #
105
- # @param epin_string [String] EPIN notation string
106
- # @return [Identifier] new identifier instance
107
- # @raise [ArgumentError] if the EPIN string is invalid
53
+ # @return [Boolean] true if derived style, false otherwise
108
54
  #
109
- # @example Parse EPIN strings
110
- # Sashite::Epin::Identifier.parse("K^") # => Native king
111
- # Sashite::Epin::Identifier.parse("K^'") # => Derived king
112
- # Sashite::Epin::Identifier.parse("+R'") # => Derived enhanced rook
113
- def self.parse(epin_string)
114
- string_value = String(epin_string)
115
- validate_epin_string(string_value)
116
-
117
- # Check for derivation marker
118
- has_marker = string_value.end_with?(DERIVATION_MARKER)
119
-
120
- # Extract PIN part (remove derivation marker if present)
121
- pin_part = has_marker ? string_value[0...-1] : string_value
122
-
123
- # Parse PIN component
124
- pin_identifier = Pin::Identifier.parse(pin_part)
125
-
126
- new(pin_identifier, derived: has_marker)
55
+ # @example
56
+ # Identifier.new(pin).derived? # => false
57
+ # Identifier.new(pin, derived: true).derived? # => true
58
+ def derived?
59
+ @derived
127
60
  end
128
61
 
129
- # Check if a string is a valid EPIN notation
62
+ # Returns the native status.
130
63
  #
131
- # @param epin_string [String] the string to validate
132
- # @return [Boolean] true if valid EPIN, false otherwise
64
+ # @return [Boolean] true if native style, false otherwise
133
65
  #
134
- # @example Validate EPIN strings
135
- # Sashite::Epin::Identifier.valid?("K^") # => true
136
- # Sashite::Epin::Identifier.valid?("K^'") # => true
137
- # Sashite::Epin::Identifier.valid?("+R'") # => true
138
- # Sashite::Epin::Identifier.valid?("K^''") # => false (multiple markers)
139
- # Sashite::Epin::Identifier.valid?("KK'") # => false (invalid PIN)
140
- def self.valid?(epin_string)
141
- return false unless epin_string.is_a?(::String)
142
- return false unless epin_string.match?(EPIN_PATTERN)
143
-
144
- # Check for multiple derivation markers
145
- marker_count = epin_string.count(DERIVATION_MARKER)
146
- return false if marker_count > 1
147
-
148
- # Validate PIN part
149
- has_marker = epin_string.end_with?(DERIVATION_MARKER)
150
- pin_part = has_marker ? epin_string[0...-1] : epin_string
151
-
152
- Pin::Identifier.valid?(pin_part)
66
+ # @example
67
+ # Identifier.new(pin).native? # => true
68
+ # Identifier.new(pin, derived: true).native? # => false
69
+ def native?
70
+ !@derived
153
71
  end
154
72
 
155
- # Convert the identifier to its EPIN string representation
73
+ # ========================================================================
74
+ # String Conversion
75
+ # ========================================================================
76
+
77
+ # Returns the EPIN string representation.
156
78
  #
157
- # @return [String] EPIN notation string
79
+ # @return [String] The EPIN string
158
80
  #
159
- # @example Serialize identifiers
160
- # native.to_s # => "K^"
161
- # derived.to_s # => "K^'"
81
+ # @example
82
+ # Identifier.new(pin).to_s # => "K^"
83
+ # Identifier.new(pin, derived: true).to_s # => "K^'"
162
84
  def to_s
163
- pin.to_s + suffix
85
+ derived? ? "#{pin}#{Constants::DERIVATION_SUFFIX}" : pin.to_s
164
86
  end
165
87
 
166
- # Get the derivation marker suffix
167
- #
168
- # @return [String] derivation marker if derived, empty string if native
169
- def suffix
170
- derived? ? DERIVATION_MARKER : ""
171
- end
88
+ # ========================================================================
89
+ # Transformations
90
+ # ========================================================================
172
91
 
173
- # Create a new identifier with a different PIN component
92
+ # Returns a new Identifier with a different PIN component.
174
93
  #
175
- # @param new_pin [Pin::Identifier] new PIN component
176
- # @return [Identifier] new identifier with different PIN
177
- # @raise [ArgumentError] if new_pin is not a Pin::Identifier
94
+ # @param new_pin [Sashite::Pin::Identifier] The new PIN component
95
+ # @return [Identifier] A new Identifier with the specified PIN
96
+ # @raise [Errors::Argument] If the PIN is invalid
178
97
  #
179
- # @example Replace PIN component
180
- # epin = Sashite::Epin.parse("K^'")
181
- # new_pin = epin.pin.with_type(:Q)
182
- # epin.with_pin(new_pin).to_s # => "Q^'"
98
+ # @example
99
+ # epin = Identifier.new(pin, derived: true)
100
+ # new_pin = Sashite::Pin.parse("+Q^")
101
+ # epin.with_pin(new_pin).to_s # => "+Q^'"
183
102
  def with_pin(new_pin)
184
- raise ::ArgumentError, format(ERROR_INVALID_PIN, new_pin.class) unless new_pin.is_a?(Pin::Identifier)
185
103
  return self if pin == new_pin
186
104
 
187
- self.class.new(new_pin, derived: derived?)
188
- end
189
-
190
- # Create a new identifier with different derivation status
191
- #
192
- # @param new_derived [Boolean] new derivation status
193
- # @return [Identifier] new identifier with different derivation
194
- #
195
- # @example Change derivation status
196
- # native = Sashite::Epin.parse("K^")
197
- # derived = native.with_derived(true)
198
- # derived.to_s # => "K^'"
199
- def with_derived(new_derived)
200
- new_derived_bool = !!new_derived
201
- return self if derived? == new_derived_bool
202
-
203
- self.class.new(pin, derived: new_derived_bool)
105
+ self.class.new(new_pin, derived: @derived)
204
106
  end
205
107
 
206
- # Create a new identifier marked as derived (opponent's native style)
108
+ # Returns a new Identifier marked as derived.
207
109
  #
208
- # @return [Identifier] new identifier marked as derived
110
+ # @return [Identifier] A new Identifier with derived: true
209
111
  #
210
- # @example Mark as derived
211
- # native = Sashite::Epin.parse("K^")
212
- # derived = native.mark_derived
213
- # derived.to_s # => "K^'"
214
- def mark_derived
112
+ # @example
113
+ # epin = Identifier.new(pin)
114
+ # epin.derive.to_s # => "K^'"
115
+ def derive
215
116
  return self if derived?
216
117
 
217
118
  self.class.new(pin, derived: true)
218
119
  end
219
120
 
220
- # Create a new identifier marked as native (own side's native style)
121
+ # Returns a new Identifier marked as native.
221
122
  #
222
- # @return [Identifier] new identifier marked as native
123
+ # @return [Identifier] A new Identifier with derived: false
223
124
  #
224
- # @example Mark as native
225
- # derived = Sashite::Epin.parse("K^'")
226
- # native = derived.unmark_native
227
- # native.to_s # => "K^"
228
- def unmark_native
229
- return self unless derived?
125
+ # @example
126
+ # epin = Identifier.new(pin, derived: true)
127
+ # epin.native.to_s # => "K^"
128
+ def native
129
+ return self if native?
230
130
 
231
131
  self.class.new(pin, derived: false)
232
132
  end
233
133
 
234
- # Check if the identifier uses native style (own side's native style)
235
- #
236
- # @return [Boolean] true if native (not derived)
237
- def native?
238
- !derived?
239
- end
134
+ # ========================================================================
135
+ # Comparison Queries
136
+ # ========================================================================
240
137
 
241
- # Check if this identifier has the same derivation status as another
138
+ # Checks if two Identifiers have the same derived status.
242
139
  #
243
- # @param other [Identifier] identifier to compare with
244
- # @return [Boolean] true if same derivation status
140
+ # @param other [Identifier] The other Identifier to compare
141
+ # @return [Boolean] true if same derived status
245
142
  #
246
- # @example Compare derivation status
247
- # native = Sashite::Epin.parse("K^")
248
- # derived = Sashite::Epin.parse("K^'")
249
- # native.same_derivation?(derived) # => false
250
- def same_derivation?(other)
251
- return false unless other.is_a?(self.class)
252
-
253
- derived? == other.derived?
143
+ # @example
144
+ # epin1 = Identifier.new(pin1, derived: true)
145
+ # epin2 = Identifier.new(pin2, derived: true)
146
+ # epin1.same_derived?(epin2) # => true
147
+ def same_derived?(other)
148
+ @derived == other.derived?
254
149
  end
255
150
 
256
- # Custom equality comparison
151
+ # ========================================================================
152
+ # Equality
153
+ # ========================================================================
154
+
155
+ # Checks equality with another Identifier.
257
156
  #
258
- # @param other [Object] object to compare with
259
- # @return [Boolean] true if identifiers are equal
157
+ # @param other [Object] The object to compare
158
+ # @return [Boolean] true if equal
260
159
  def ==(other)
261
- return false unless other.is_a?(self.class)
160
+ return false unless self.class === other
262
161
 
263
- pin == other.pin && derived? == other.derived?
162
+ pin == other.pin && @derived == other.derived?
264
163
  end
265
164
 
266
- # Alias for == to ensure Set functionality works correctly
267
165
  alias eql? ==
268
166
 
269
- # Custom hash implementation for use in collections
167
+ # Returns a hash code for the Identifier.
270
168
  #
271
- # @return [Integer] hash value
169
+ # @return [Integer] Hash code
272
170
  def hash
273
- [self.class, pin, derived?].hash
171
+ [pin, @derived].hash
274
172
  end
275
173
 
276
- # Validate EPIN string format
174
+ # Returns an inspect string for the Identifier.
277
175
  #
278
- # @param string [String] string to validate
279
- # @raise [ArgumentError] if string doesn't match EPIN pattern or has multiple markers
280
- def self.validate_epin_string(string)
281
- raise ::ArgumentError, format(ERROR_INVALID_EPIN, string) unless string.match?(EPIN_PATTERN)
176
+ # @return [String] Inspect representation
177
+ def inspect
178
+ "#<#{self.class} #{self}>"
179
+ end
180
+
181
+ private
182
+
183
+ # ========================================================================
184
+ # Private Validation
185
+ # ========================================================================
282
186
 
283
- # Check for multiple derivation markers
284
- marker_count = string.count(DERIVATION_MARKER)
285
- return unless marker_count > 1
187
+ def validate_pin!(pin)
188
+ return if ::Sashite::Pin::Identifier === pin
286
189
 
287
- raise ::ArgumentError, format(ERROR_MULTIPLE_MARKERS, string)
190
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_PIN
288
191
  end
289
192
 
290
- private_class_method :validate_epin_string
193
+ def validate_derived!(derived)
194
+ return if ::TrueClass === derived || ::FalseClass === derived
195
+
196
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_DERIVED
197
+ end
291
198
  end
292
199
  end
293
200
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+ require_relative "errors"
5
+
6
+ module Sashite
7
+ module Epin
8
+ # Parser for EPIN (Extended Piece Identifier Notation) strings.
9
+ #
10
+ # This parser extracts the derivation marker and delegates PIN parsing
11
+ # to the sashite-pin library.
12
+ #
13
+ # @example
14
+ # Parser.parse("K") # => { pin: { abbr: :K, side: :first, ... }, derived: false }
15
+ # Parser.parse("K^'") # => { pin: { abbr: :K, side: :first, ..., terminal: true }, derived: true }
16
+ #
17
+ # @see https://sashite.dev/specs/epin/1.0.0/
18
+ module Parser
19
+ # Parses an EPIN string into its components.
20
+ #
21
+ # @param input [String] The EPIN string to parse
22
+ # @return [Hash] A hash with :pin (PIN components hash) and :derived keys
23
+ # @raise [Errors::Argument] If the input is not a valid EPIN string
24
+ def self.parse(input)
25
+ validate_string!(input)
26
+
27
+ derived = has_derivation_marker?(input)
28
+
29
+ if derived
30
+ validate_derivation_marker!(input)
31
+ pin_string = input.chop
32
+ else
33
+ pin_string = input
34
+ end
35
+
36
+ pin_components = parse_pin_component(pin_string)
37
+
38
+ { pin: pin_components, derived: derived }
39
+ end
40
+
41
+ # Validates an EPIN string without raising an exception.
42
+ #
43
+ # @param input [String] The EPIN string to validate
44
+ # @return [Boolean] true if valid, false otherwise
45
+ def self.valid?(input)
46
+ return false unless ::String === input
47
+
48
+ parse(input)
49
+ true
50
+ rescue Errors::Argument
51
+ false
52
+ end
53
+
54
+ class << self
55
+ private
56
+
57
+ # Validates that input is a String.
58
+ #
59
+ # @param input [Object] The input to validate
60
+ # @raise [Errors::Argument] If input is not a String
61
+ def validate_string!(input)
62
+ return if ::String === input
63
+
64
+ raise Errors::Argument, "invalid PIN component: must contain exactly one letter"
65
+ end
66
+
67
+ # Checks if the input contains a derivation marker.
68
+ #
69
+ # @param input [String] The input to check
70
+ # @return [Boolean] true if contains derivation marker
71
+ def has_derivation_marker?(input)
72
+ input.include?(Constants::DERIVATION_SUFFIX)
73
+ end
74
+
75
+ # Validates derivation marker position and uniqueness.
76
+ #
77
+ # @param input [String] The input to validate
78
+ # @raise [Errors::Argument] If derivation marker is invalid
79
+ def validate_derivation_marker!(input)
80
+ count = input.count(Constants::DERIVATION_SUFFIX)
81
+ last_char = input[-1]
82
+
83
+ return if count == 1 && last_char == Constants::DERIVATION_SUFFIX
84
+
85
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_DERIVATION_MARKER
86
+ end
87
+
88
+ # Parses the PIN component using sashite-pin.
89
+ #
90
+ # @param pin_string [String] The PIN string to parse
91
+ # @return [Hash] PIN components hash
92
+ # @raise [Errors::Argument] If PIN parsing fails
93
+ def parse_pin_component(pin_string)
94
+ ::Sashite::Pin::Parser.parse(pin_string)
95
+ rescue ::Sashite::Pin::Errors::Argument => e
96
+ raise Errors::Argument, "invalid PIN component: #{e.message}"
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end