sashite-epin 2.1.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,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+ require_relative "errors"
5
+
6
+ module Sashite
7
+ module Epin
8
+ # Represents a parsed EPIN (Extended Piece Identifier Notation) identifier.
9
+ #
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
13
+ #
14
+ # Instances are immutable (frozen after creation).
15
+ #
16
+ # @example Creating identifiers
17
+ # pin = Sashite::Pin.parse("K^")
18
+ # epin = Identifier.new(pin)
19
+ # epin = Identifier.new(pin, derived: true)
20
+ #
21
+ # @example String conversion
22
+ # Identifier.new(pin).to_s # => "K^"
23
+ # Identifier.new(pin, derived: true).to_s # => "K^'"
24
+ #
25
+ # @see https://sashite.dev/specs/epin/1.0.0/
26
+ class Identifier
27
+ # @return [Sashite::Pin::Identifier] PIN component
28
+ attr_reader :pin
29
+
30
+ # Creates a new Identifier instance.
31
+ #
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
36
+ #
37
+ # @example
38
+ # pin = Sashite::Pin.parse("K^")
39
+ # Identifier.new(pin)
40
+ # Identifier.new(pin, derived: true)
41
+ def initialize(pin, derived: false)
42
+ validate_pin!(pin)
43
+ validate_derived!(derived)
44
+
45
+ @pin = pin
46
+ @derived = derived
47
+
48
+ freeze
49
+ end
50
+
51
+ # Returns the derived status.
52
+ #
53
+ # @return [Boolean] true if derived style, false otherwise
54
+ #
55
+ # @example
56
+ # Identifier.new(pin).derived? # => false
57
+ # Identifier.new(pin, derived: true).derived? # => true
58
+ def derived?
59
+ @derived
60
+ end
61
+
62
+ # Returns the native status.
63
+ #
64
+ # @return [Boolean] true if native style, false otherwise
65
+ #
66
+ # @example
67
+ # Identifier.new(pin).native? # => true
68
+ # Identifier.new(pin, derived: true).native? # => false
69
+ def native?
70
+ !@derived
71
+ end
72
+
73
+ # ========================================================================
74
+ # String Conversion
75
+ # ========================================================================
76
+
77
+ # Returns the EPIN string representation.
78
+ #
79
+ # @return [String] The EPIN string
80
+ #
81
+ # @example
82
+ # Identifier.new(pin).to_s # => "K^"
83
+ # Identifier.new(pin, derived: true).to_s # => "K^'"
84
+ def to_s
85
+ derived? ? "#{pin}#{Constants::DERIVATION_SUFFIX}" : pin.to_s
86
+ end
87
+
88
+ # ========================================================================
89
+ # Transformations
90
+ # ========================================================================
91
+
92
+ # Returns a new Identifier with a different PIN component.
93
+ #
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
97
+ #
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^'"
102
+ def with_pin(new_pin)
103
+ return self if pin == new_pin
104
+
105
+ self.class.new(new_pin, derived: @derived)
106
+ end
107
+
108
+ # Returns a new Identifier marked as derived.
109
+ #
110
+ # @return [Identifier] A new Identifier with derived: true
111
+ #
112
+ # @example
113
+ # epin = Identifier.new(pin)
114
+ # epin.derive.to_s # => "K^'"
115
+ def derive
116
+ return self if derived?
117
+
118
+ self.class.new(pin, derived: true)
119
+ end
120
+
121
+ # Returns a new Identifier marked as native.
122
+ #
123
+ # @return [Identifier] A new Identifier with derived: false
124
+ #
125
+ # @example
126
+ # epin = Identifier.new(pin, derived: true)
127
+ # epin.native.to_s # => "K^"
128
+ def native
129
+ return self if native?
130
+
131
+ self.class.new(pin, derived: false)
132
+ end
133
+
134
+ # ========================================================================
135
+ # Comparison Queries
136
+ # ========================================================================
137
+
138
+ # Checks if two Identifiers have the same derived status.
139
+ #
140
+ # @param other [Identifier] The other Identifier to compare
141
+ # @return [Boolean] true if same derived status
142
+ #
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?
149
+ end
150
+
151
+ # ========================================================================
152
+ # Equality
153
+ # ========================================================================
154
+
155
+ # Checks equality with another Identifier.
156
+ #
157
+ # @param other [Object] The object to compare
158
+ # @return [Boolean] true if equal
159
+ def ==(other)
160
+ return false unless self.class === other
161
+
162
+ pin == other.pin && @derived == other.derived?
163
+ end
164
+
165
+ alias eql? ==
166
+
167
+ # Returns a hash code for the Identifier.
168
+ #
169
+ # @return [Integer] Hash code
170
+ def hash
171
+ [pin, @derived].hash
172
+ end
173
+
174
+ # Returns an inspect string for the Identifier.
175
+ #
176
+ # @return [String] Inspect representation
177
+ def inspect
178
+ "#<#{self.class} #{self}>"
179
+ end
180
+
181
+ private
182
+
183
+ # ========================================================================
184
+ # Private Validation
185
+ # ========================================================================
186
+
187
+ def validate_pin!(pin)
188
+ return if ::Sashite::Pin::Identifier === pin
189
+
190
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_PIN
191
+ end
192
+
193
+ def validate_derived!(derived)
194
+ return if ::TrueClass === derived || ::FalseClass === derived
195
+
196
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_DERIVED
197
+ end
198
+ end
199
+ end
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
data/lib/sashite/epin.rb CHANGED
@@ -2,300 +2,79 @@
2
2
 
3
3
  require "sashite/pin"
4
4
 
5
+ require_relative "epin/constants"
6
+ require_relative "epin/errors"
7
+ require_relative "epin/identifier"
8
+ require_relative "epin/parser"
9
+
5
10
  module Sashite
6
11
  # EPIN (Extended Piece Identifier Notation) implementation for Ruby.
7
12
  #
8
- # EPIN extends PIN by adding a **derivation marker** to track piece style
9
- # in cross-style games.
10
- #
11
- # **EPIN is simply: PIN + optional style derivation marker (`'`)**
13
+ # EPIN extends PIN with an optional derivation marker (') that flags
14
+ # whether a piece uses a native or derived style.
12
15
  #
13
16
  # == Format
14
17
  #
15
- # <pin-token>[<derivation-marker>]
16
- #
17
- # Where +<pin-token>+ is a valid PIN token and +<derivation-marker>+ is
18
- # an optional trailing apostrophe (<tt>'</tt>).
18
+ # <pin>[']
19
19
  #
20
- # == Five Fundamental Attributes
21
- #
22
- # EPIN exposes all five attributes from the Sashité Game Protocol:
23
- #
24
- # - *Piece Name* â†' +epin.pin.type+
25
- # - *Piece Side* â†' +epin.pin.side+
26
- # - *Piece State* â†' +epin.pin.state+
27
- # - *Terminal Status* â†' +epin.pin.terminal+
28
- # - *Piece Style* â†' +epin.derived+ (native vs derived)
20
+ # - *PIN*: Any valid PIN token (abbr, side, state, terminal)
21
+ # - *Derivation marker*: <tt>'</tt> (derived) or absent (native)
29
22
  #
30
23
  # == Examples
31
24
  #
32
25
  # epin = Sashite::Epin.parse("K^'")
33
- # epin.pin.type # => :K
34
- # epin.pin.terminal # => true
35
- # epin.derived # => true
26
+ # epin.pin.abbr # => :K
27
+ # epin.pin.side # => :first
28
+ # epin.pin.terminal? # => true
29
+ # epin.derived? # => true
36
30
  #
37
- # pin = Sashite::Pin.parse("K^")
38
- # epin = Sashite::Epin.new(pin, derived: true)
39
- # epin.to_s # => "K^'"
31
+ # epin = Sashite::Epin.parse("+R")
32
+ # epin.to_s # => "+R"
40
33
  #
41
- # Sashite::Epin.valid?("K^'") # => true
42
- # Sashite::Epin.valid?("K'^") # => false
34
+ # Sashite::Epin.valid?("K^'") # => true
35
+ # Sashite::Epin.valid?("invalid") # => false
43
36
  #
44
- # See the EPIN Specification (https://sashite.dev/specs/epin/1.0.0/) for details.
45
- class Epin
46
- # Pattern for validating EPIN strings
47
- EPIN_PATTERN = /\A(?<pin>[-+]?[A-Za-z]\^?)(?<derived>')?\z/
48
-
49
- # @return [Sashite::Pin] The underlying PIN component
50
- attr_reader :pin
51
-
52
- # @return [Boolean] Derivation status (true = derived, false = native)
53
- attr_reader :derived
54
-
55
- # ========================================================================
56
- # Creation and Parsing
57
- # ========================================================================
58
-
59
- # Creates a new EPIN instance from a PIN component.
37
+ # @see https://sashite.dev/specs/epin/1.0.0/
38
+ module Epin
39
+ # Parses an EPIN string into an Identifier.
60
40
  #
61
- # @param pin [Sashite::Pin] The underlying PIN instance
62
- # @param derived [Boolean] Derivation status (default: false)
63
- # @return [Epin] A new frozen Epin instance
64
- #
65
- # @example
66
- # pin = Sashite::Pin.parse("K^")
67
- # Sashite::Epin.new(pin)
68
- # # => #<Sashite::Epin K^>
69
- #
70
- # Sashite::Epin.new(pin, derived: true)
71
- # # => #<Sashite::Epin K^'>
72
- def initialize(pin, derived: false)
73
- raise ArgumentError, "Expected a Sashite::Pin instance, got: #{pin.inspect}" unless pin.is_a?(Pin)
74
-
75
- @pin = pin
76
- @derived = !!derived
77
-
78
- freeze
79
- end
80
-
81
- # Parses an EPIN string into an Epin instance.
82
- #
83
- # @param epin_string [String] The EPIN string to parse
84
- # @return [Epin] A new Epin instance
85
- # @raise [ArgumentError] If the string is not a valid EPIN
41
+ # @param string [String] The EPIN string to parse
42
+ # @return [Identifier] A new Identifier instance
43
+ # @raise [Errors::Argument] If the string is not a valid EPIN
86
44
  #
87
45
  # @example
88
46
  # Sashite::Epin.parse("K")
89
- # # => #<Sashite::Epin K>
90
- #
91
- # Sashite::Epin.parse("K'")
92
- # # => #<Sashite::Epin K'>
47
+ # # => #<Sashite::Epin::Identifier K>
93
48
  #
94
- # Sashite::Epin.parse("+R^'")
95
- # # => #<Sashite::Epin +R^'>
49
+ # Sashite::Epin.parse("K^'")
50
+ # # => #<Sashite::Epin::Identifier K^'>
96
51
  #
97
52
  # Sashite::Epin.parse("invalid")
98
- # # => ArgumentError: Invalid EPIN string: invalid
99
- def self.parse(epin_string)
100
- raise ArgumentError, "Invalid EPIN string: #{epin_string.inspect}" unless epin_string.is_a?(String)
101
-
102
- match = EPIN_PATTERN.match(epin_string)
103
- raise ArgumentError, "Invalid EPIN string: #{epin_string}" unless match
104
-
105
- pin_string = match[:pin]
106
- derived_marker = match[:derived]
107
-
108
- pin = Pin.parse(pin_string)
109
- derived = derived_marker == "'"
110
-
111
- new(pin, derived: derived)
53
+ # # => raises Errors::Argument
54
+ def self.parse(string)
55
+ components = Parser.parse(string)
56
+
57
+ pin = ::Sashite::Pin::Identifier.new(
58
+ components[:pin][:abbr],
59
+ components[:pin][:side],
60
+ components[:pin][:state],
61
+ terminal: components[:pin][:terminal]
62
+ )
63
+
64
+ Identifier.new(pin, derived: components[:derived])
112
65
  end
113
66
 
114
67
  # Checks if a string is a valid EPIN notation.
115
68
  #
116
- # @param epin_string [String] The string to validate
69
+ # @param string [String] The string to validate
117
70
  # @return [Boolean] true if valid, false otherwise
118
71
  #
119
72
  # @example
120
- # Sashite::Epin.valid?("K") # => true
121
- # Sashite::Epin.valid?("K'") # => true
122
- # Sashite::Epin.valid?("+R^'") # => true
123
- # Sashite::Epin.valid?("K'^") # => false
124
- # Sashite::Epin.valid?("K''") # => false
125
- # Sashite::Epin.valid?("invalid") # => false
126
- def self.valid?(epin_string)
127
- return false unless epin_string.is_a?(String)
128
-
129
- EPIN_PATTERN.match?(epin_string)
130
- end
131
-
132
- # ========================================================================
133
- # Conversion
134
- # ========================================================================
135
-
136
- # Converts the Epin to its string representation.
137
- #
138
- # @return [String] The EPIN string
139
- #
140
- # @example
141
- # pin = Sashite::Pin.parse("K^")
142
- # Sashite::Epin.new(pin).to_s
143
- # # => "K^"
144
- #
145
- # Sashite::Epin.new(pin, derived: true).to_s
146
- # # => "K^'"
147
- def to_s
148
- "#{pin}#{derivation_suffix}"
149
- end
150
-
151
- # ========================================================================
152
- # Transformations
153
- # ========================================================================
154
-
155
- # Returns a new Epin with a different PIN component.
156
- #
157
- # @param new_pin [Sashite::Pin] The new PIN component
158
- # @return [Epin] A new Epin with the specified PIN
159
- #
160
- # @example
161
- # epin = Sashite::Epin.parse("K^'")
162
- # new_pin = epin.pin.with_type(:Q)
163
- # epin.with_pin(new_pin).to_s
164
- # # => "Q^'"
165
- def with_pin(new_pin)
166
- return self if pin == new_pin
167
-
168
- self.class.new(new_pin, derived: derived)
169
- end
170
-
171
- # Returns a new Epin with a different derivation status.
172
- #
173
- # @param new_derived [Boolean] The new derivation status
174
- # @return [Epin] A new Epin with the specified derivation status
175
- #
176
- # @example
177
- # epin = Sashite::Epin.parse("K^")
178
- # epin.with_derived(true).to_s
179
- # # => "K^'"
180
- #
181
- # epin = Sashite::Epin.parse("K^'")
182
- # epin.with_derived(false).to_s
183
- # # => "K^"
184
- def with_derived(new_derived)
185
- return self if derived == !!new_derived
186
-
187
- self.class.new(pin, derived: !!new_derived)
188
- end
189
-
190
- # Returns a new Epin marked as derived.
191
- #
192
- # @return [Epin] A new Epin with derived: true
193
- #
194
- # @example
195
- # epin = Sashite::Epin.parse("K^")
196
- # epin.mark_derived.derived
197
- # # => true
198
- def mark_derived
199
- return self if derived
200
-
201
- self.class.new(pin, derived: true)
202
- end
203
-
204
- # Returns a new Epin marked as native (not derived).
205
- #
206
- # @return [Epin] A new Epin with derived: false
207
- #
208
- # @example
209
- # epin = Sashite::Epin.parse("K^'")
210
- # epin.unmark_derived.derived
211
- # # => false
212
- def unmark_derived
213
- return self unless derived
214
-
215
- self.class.new(pin, derived: false)
216
- end
217
-
218
- # ========================================================================
219
- # Queries
220
- # ========================================================================
221
-
222
- # Checks if the Epin is derived (uses opponent's style).
223
- #
224
- # @return [Boolean] true if derived
225
- #
226
- # @example
227
- # Sashite::Epin.parse("K^'").derived? # => true
228
- # Sashite::Epin.parse("K^").derived? # => false
229
- def derived?
230
- derived
231
- end
232
-
233
- # Checks if the Epin is native (uses own side's style).
234
- #
235
- # @return [Boolean] true if native
236
- #
237
- # @example
238
- # Sashite::Epin.parse("K^").native? # => true
239
- # Sashite::Epin.parse("K^'").native? # => false
240
- def native?
241
- !derived
242
- end
243
-
244
- # Checks if two Epins have the same derivation status.
245
- #
246
- # @param other [Epin] The other Epin to compare
247
- # @return [Boolean] true if same derivation status
248
- #
249
- # @example
250
- # epin1 = Sashite::Epin.parse("K^'")
251
- # epin2 = Sashite::Epin.parse("Q'")
252
- # epin1.same_derived?(epin2)
253
- # # => true
254
- #
255
- # epin3 = Sashite::Epin.parse("K^")
256
- # epin1.same_derived?(epin3)
257
- # # => false
258
- def same_derived?(other)
259
- derived == other.derived
260
- end
261
-
262
- # ========================================================================
263
- # Comparison
264
- # ========================================================================
265
-
266
- # Checks equality with another Epin.
267
- #
268
- # @param other [Object] The object to compare
269
- # @return [Boolean] true if equal
270
- def ==(other)
271
- return false unless other.is_a?(self.class)
272
-
273
- pin == other.pin && derived == other.derived
274
- end
275
-
276
- alias eql? ==
277
-
278
- # Returns a hash code for the Epin.
279
- #
280
- # @return [Integer] Hash code
281
- def hash
282
- [pin, derived].hash
283
- end
284
-
285
- # Returns an inspect string for the Epin.
286
- #
287
- # @return [String] Inspect representation
288
- def inspect
289
- "#<#{self.class} #{self}>"
290
- end
291
-
292
- private
293
-
294
- # Returns the derivation suffix for string representation.
295
- #
296
- # @return [String] "'" if derived, "" otherwise
297
- def derivation_suffix
298
- derived ? "'" : ""
73
+ # Sashite::Epin.valid?("K") # => true
74
+ # Sashite::Epin.valid?("K^'") # => true
75
+ # Sashite::Epin.valid?("invalid") # => false
76
+ def self.valid?(string)
77
+ Parser.valid?(string)
299
78
  end
300
79
  end
301
80
  end
data/lib/sashite-epin.rb CHANGED
@@ -1,14 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "sashite/epin"
4
-
5
- # Sashité namespace for board game notation libraries
6
- #
7
- # Sashité provides a collection of libraries for representing and manipulating
8
- # board game concepts according to the Game Protocol specifications.
9
- #
10
- # @see https://sashite.dev/game-protocol/ Game Protocol Foundation
11
- # @see https://sashite.dev/specs/ Sashité Specifications
12
- # @author Sashité
13
- module Sashite
14
- end