Class: Sashite::Qpi::Identifier

Inherits:
Object
  • Object
show all
Defined in:
lib/sashite/qpi/identifier.rb

Overview

Represents an identifier in QPI (Qualified Piece Identifier) format.

A QPI identifier combines style and piece attributes into a unified representation:

  • Family: Style family from SIN component (:A to :Z only)

  • Type: Piece type (:A to :Z) from PIN component

  • Side: Player assignment (:first or :second) from both components

  • State: Piece state (:normal, :enhanced, :diminished) from PIN component

  • Semantic constraint: SIN and PIN components must represent the same player

All instances are immutable - transformation methods return new instances. This follows the QPI Specification v1.0.0 with strict parameter validation consistent with the underlying SIN and PIN primitive specifications.

## Strict Parameter Validation

QPI enforces the same strict validation as its underlying primitives:

  • Family parameter must be a symbol from :A to :Z (not :a to :z)

  • Type parameter must be a symbol from :A to :Z (delegated to PIN)

  • Side parameter determines the display case, not the input parameters

This ensures consistency with SIN and PIN behavior where lowercase symbols are rejected with ArgumentError.

Examples:

Strict parameter validation

# Valid - uppercase symbols only
Sashite::Qpi::Identifier.new(:C, :K, :first, :normal)   # => "C:K"
Sashite::Qpi::Identifier.new(:C, :K, :second, :normal)  # => "c:k"

# Invalid - lowercase symbols rejected
Sashite::Qpi::Identifier.new(:c, :K, :second, :normal)  # => ArgumentError
Sashite::Qpi::Identifier.new(:C, :k, :second, :normal)  # => ArgumentError

See Also:

Constant Summary collapse

SEPARATOR =

Component separator for string representation

":"
ERROR_INVALID_QPI =

Error messages

"Invalid QPI string: %s"
ERROR_SEMANTIC_MISMATCH =
"Family and side must represent the same player: family=%s (side=%s), side=%s"
ERROR_MISSING_SEPARATOR =
"QPI string must contain exactly one colon separator: %s"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(family, type, side, state = Pin::Identifier::NORMAL_STATE) ⇒ Identifier

Create a new identifier instance

Examples:

Create identifiers with strict parameter validation

# Valid - uppercase symbols only
chess_king = Sashite::Qpi::Identifier.new(:C, :K, :first, :normal)   # => "C:K"
chess_pawn = Sashite::Qpi::Identifier.new(:C, :P, :second, :normal)  # => "c:p"

# Invalid - lowercase symbols rejected
# Sashite::Qpi::Identifier.new(:c, :K, :first, :normal)   # => ArgumentError
# Sashite::Qpi::Identifier.new(:C, :k, :first, :normal)   # => ArgumentError

Parameters:

  • family (Symbol)

    style family identifier (:A to :Z only)

  • type (Symbol)

    piece type (:A to :Z only)

  • side (Symbol)

    player side (:first or :second)

  • state (Symbol) (defaults to: Pin::Identifier::NORMAL_STATE)

    piece state (:normal, :enhanced, or :diminished)

Raises:

  • (ArgumentError)

    if parameters are invalid or semantically inconsistent



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/sashite/qpi/identifier.rb', line 86

def initialize(family, type, side, state = Pin::Identifier::NORMAL_STATE)
  # Strict validation - delegate to underlying primitives for consistency
  Sin::Identifier.validate_family(family)
  Pin::Identifier.validate_type(type)
  Pin::Identifier.validate_side(side)
  Pin::Identifier.validate_state(state)

  # Create PIN component
  @pin_identifier = Pin::Identifier.new(type, side, state)

  # Create SIN component - pass family directly without normalization
  @sin_identifier = Sin::Identifier.new(family, side)

  # Validate semantic consistency
  validate_semantic_consistency

  freeze
end

Class Method Details

.parse(qpi_string) ⇒ Identifier

Parse a QPI string into an Identifier object

Examples:

Parse QPI strings with automatic component separation

Sashite::Qpi::Identifier.parse("C:K")   # => #<Qpi::Identifier family=:C type=:K side=:first state=:normal>
Sashite::Qpi::Identifier.parse("s:+r")  # => #<Qpi::Identifier family=:S type=:R side=:second state=:enhanced>
Sashite::Qpi::Identifier.parse("X:-S")  # => #<Qpi::Identifier family=:X type=:S side=:first state=:diminished>

Parameters:

  • qpi_string (String)

    QPI notation string (format: sin:pin)

Returns:

Raises:

  • (ArgumentError)

    if the QPI string is invalid



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/sashite/qpi/identifier.rb', line 115

def self.parse(qpi_string)
  string_value = String(qpi_string)
  sin_part, pin_part = split_components(string_value)

  # Parse components
  sin_identifier = Sin::Identifier.parse(sin_part)
  pin_identifier = Pin::Identifier.parse(pin_part)

  # Validate semantic consistency BEFORE creating new instance
  unless sin_identifier.side == pin_identifier.side
    raise ::ArgumentError, format(ERROR_SEMANTIC_MISMATCH,
                                  sin_part, sin_identifier.side, pin_identifier.side)
  end

  # Extract parameters and create new instance
  new(sin_identifier.family, pin_identifier.type, pin_identifier.side, pin_identifier.state)
end

.valid?(qpi_string) ⇒ Boolean

Check if a string is a valid QPI notation

Examples:

Validate QPI strings with semantic checking

Sashite::Qpi::Identifier.valid?("C:K")     # => true
Sashite::Qpi::Identifier.valid?("s:+r")    # => true
Sashite::Qpi::Identifier.valid?("C:k")     # => false (semantic mismatch)
Sashite::Qpi::Identifier.valid?("Chess")   # => false (no separator)

Parameters:

  • qpi_string (String)

    the string to validate

Returns:

  • (Boolean)

    true if valid QPI, false otherwise



143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/sashite/qpi/identifier.rb', line 143

def self.valid?(qpi_string)
  return false unless qpi_string.is_a?(::String)

  # Split components and validate each part
  sin_part, pin_part = split_components(qpi_string)
  return false unless Sashite::Sin.valid?(sin_part) && Sashite::Pin.valid?(pin_part)

  # Semantic consistency check
  sin_identifier = Sashite::Sin.parse(sin_part)
  pin_identifier = Sashite::Pin.parse(pin_part)
  sin_identifier.side == pin_identifier.side
rescue ArgumentError
  false
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Custom equality comparison

Parameters:

  • other (Object)

    object to compare with

Returns:

  • (Boolean)

    true if identifiers are equal



372
373
374
375
376
# File 'lib/sashite/qpi/identifier.rb', line 372

def ==(other)
  return false unless other.is_a?(self.class)

  @sin_identifier == other.sin_component && @pin_identifier == other.pin_component
end

#cross_family?(other) ⇒ Boolean

Check if this identifier has different family from another

Parameters:

  • other (Identifier)

    identifier to compare with

Returns:

  • (Boolean)

    true if different families



332
333
334
335
336
# File 'lib/sashite/qpi/identifier.rb', line 332

def cross_family?(other)
  return false unless other.is_a?(self.class)

  !same_family?(other)
end

#diminishIdentifier

Create a new identifier with diminished state

Returns:

  • (Identifier)

    new identifier with diminished PIN component



211
212
213
214
215
# File 'lib/sashite/qpi/identifier.rb', line 211

def diminish
  return self if diminished?

  self.class.new(family, type, side, Pin::Identifier::DIMINISHED_STATE)
end

#diminished?Boolean

Check if the identifier has diminished state

Returns:

  • (Boolean)

    true if diminished state



300
301
302
# File 'lib/sashite/qpi/identifier.rb', line 300

def diminished?
  @pin_identifier.diminished?
end

#enhanceIdentifier

Create a new identifier with enhanced state

Returns:

  • (Identifier)

    new identifier with enhanced PIN component



202
203
204
205
206
# File 'lib/sashite/qpi/identifier.rb', line 202

def enhance
  return self if enhanced?

  self.class.new(family, type, side, Pin::Identifier::ENHANCED_STATE)
end

#enhanced?Boolean

Check if the identifier has enhanced state

Returns:

  • (Boolean)

    true if enhanced state



293
294
295
# File 'lib/sashite/qpi/identifier.rb', line 293

def enhanced?
  @pin_identifier.enhanced?
end

#familySymbol

Returns the style family (:A to :Z based on SIN component).

Returns:

  • (Symbol)

    the style family (:A to :Z based on SIN component)



51
52
53
# File 'lib/sashite/qpi/identifier.rb', line 51

def family
  @sin_identifier.family
end

#first_player?Boolean

Check if the identifier belongs to the first player

Returns:

  • (Boolean)

    true if first player



307
308
309
# File 'lib/sashite/qpi/identifier.rb', line 307

def first_player?
  @pin_identifier.first_player?
end

#flipIdentifier

Create a new identifier with opposite player assignment

Changes the player assignment (side) while preserving the family and piece attributes. This maintains semantic consistency between the components.

Examples:

Flip player assignment while preserving family and attributes

chess_first = Sashite::Qpi::Identifier.parse("C:K")   # Chess king, first player
chess_second = chess_first.flip                       # => "c:k" (Chess king, second player)

shogi_first = Sashite::Qpi::Identifier.parse("S:+R")  # Shogi enhanced rook, first player
shogi_second = shogi_first.flip                       # => "s:+r" (Shogi enhanced rook, second player)

Returns:

  • (Identifier)

    new identifier with opposite side but same family



279
280
281
# File 'lib/sashite/qpi/identifier.rb', line 279

def flip
  self.class.new(family, type, opposite_side, state)
end

#hashInteger

Custom hash implementation for use in collections

Returns:

  • (Integer)

    hash value



384
385
386
# File 'lib/sashite/qpi/identifier.rb', line 384

def hash
  [self.class, @sin_identifier, @pin_identifier].hash
end

#normal?Boolean

Check if the identifier has normal state

Returns:

  • (Boolean)

    true if normal state



286
287
288
# File 'lib/sashite/qpi/identifier.rb', line 286

def normal?
  @pin_identifier.normal?
end

#normalizeIdentifier

Create a new identifier with normal state (no modifiers)

Returns:

  • (Identifier)

    new identifier with normalized PIN component



220
221
222
223
224
# File 'lib/sashite/qpi/identifier.rb', line 220

def normalize
  return self if normal?

  self.class.new(family, type, side, Pin::Identifier::NORMAL_STATE)
end

#pin_componentSashite::Pin::Identifier

Get the parsed PIN identifier object

Returns:

  • (Sashite::Pin::Identifier)

    PIN component as identifier object



195
196
197
# File 'lib/sashite/qpi/identifier.rb', line 195

def pin_component
  @pin_identifier
end

#same_family?(other) ⇒ Boolean

Check if this identifier has the same family as another

Parameters:

  • other (Identifier)

    identifier to compare with

Returns:

  • (Boolean)

    true if same family (case-insensitive)



322
323
324
325
326
# File 'lib/sashite/qpi/identifier.rb', line 322

def same_family?(other)
  return false unless other.is_a?(self.class)

  @sin_identifier.same_family?(other.sin_component)
end

#same_side?(other) ⇒ Boolean

Check if this identifier has the same side as another

Parameters:

  • other (Identifier)

    identifier to compare with

Returns:

  • (Boolean)

    true if same side



342
343
344
345
346
# File 'lib/sashite/qpi/identifier.rb', line 342

def same_side?(other)
  return false unless other.is_a?(self.class)

  @pin_identifier.same_side?(other.pin_component)
end

#same_state?(other) ⇒ Boolean

Check if this identifier has the same state as another

Parameters:

  • other (Identifier)

    identifier to compare with

Returns:

  • (Boolean)

    true if same state



362
363
364
365
366
# File 'lib/sashite/qpi/identifier.rb', line 362

def same_state?(other)
  return false unless other.is_a?(self.class)

  @pin_identifier.same_state?(other.pin_component)
end

#same_type?(other) ⇒ Boolean

Check if this identifier has the same type as another

Parameters:

  • other (Identifier)

    identifier to compare with

Returns:

  • (Boolean)

    true if same type



352
353
354
355
356
# File 'lib/sashite/qpi/identifier.rb', line 352

def same_type?(other)
  return false unless other.is_a?(self.class)

  @pin_identifier.same_type?(other.pin_component)
end

#second_player?Boolean

Check if the identifier belongs to the second player

Returns:

  • (Boolean)

    true if second player



314
315
316
# File 'lib/sashite/qpi/identifier.rb', line 314

def second_player?
  @pin_identifier.second_player?
end

#sideSymbol

Returns the player side (:first or :second).

Returns:

  • (Symbol)

    the player side (:first or :second)



61
62
63
# File 'lib/sashite/qpi/identifier.rb', line 61

def side
  @pin_identifier.side
end

#sin_componentSashite::Sin::Identifier

Get the parsed SIN identifier object

Returns:

  • (Sashite::Sin::Identifier)

    SIN component as identifier object



188
189
190
# File 'lib/sashite/qpi/identifier.rb', line 188

def sin_component
  @sin_identifier
end

#stateSymbol

Returns the piece state (:normal, :enhanced, or :diminished).

Returns:

  • (Symbol)

    the piece state (:normal, :enhanced, or :diminished)



66
67
68
# File 'lib/sashite/qpi/identifier.rb', line 66

def state
  @pin_identifier.state
end

#to_pinString

Convert to PIN string representation (piece component only)

Examples:

Extract piece component

identifier.to_pin  # => "+K"

Returns:

  • (String)

    PIN notation string



181
182
183
# File 'lib/sashite/qpi/identifier.rb', line 181

def to_pin
  @pin_identifier.to_s
end

#to_sString

Convert the identifier to its QPI string representation

Examples:

Display QPI identifiers

identifier.to_s  # => "C:K"

Returns:

  • (String)

    QPI notation string (format: sin:pin)



163
164
165
# File 'lib/sashite/qpi/identifier.rb', line 163

def to_s
  "#{@sin_identifier}#{SEPARATOR}#{@pin_identifier}"
end

#to_sinString

Convert to SIN string representation (style component only)

Examples:

Extract style component

identifier.to_sin  # => "C"

Returns:

  • (String)

    SIN notation string



172
173
174
# File 'lib/sashite/qpi/identifier.rb', line 172

def to_sin
  @sin_identifier.to_s
end

#typeSymbol

Returns the piece type (:A to :Z).

Returns:

  • (Symbol)

    the piece type (:A to :Z)



56
57
58
# File 'lib/sashite/qpi/identifier.rb', line 56

def type
  @pin_identifier.type
end

#with_family(new_family) ⇒ Identifier

Create a new identifier with different family

Parameters:

  • new_family (Symbol)

    new style family identifier (:A to :Z)

Returns:

  • (Identifier)

    new identifier with different family



260
261
262
263
264
# File 'lib/sashite/qpi/identifier.rb', line 260

def with_family(new_family)
  return self if family == new_family

  self.class.new(new_family, type, side, state)
end

#with_side(new_side) ⇒ Identifier

Create a new identifier with different side

Parameters:

  • new_side (Symbol)

    new player side (:first or :second)

Returns:

  • (Identifier)

    new identifier with different side



240
241
242
243
244
# File 'lib/sashite/qpi/identifier.rb', line 240

def with_side(new_side)
  return self if side == new_side

  self.class.new(family, type, new_side, state)
end

#with_state(new_state) ⇒ Identifier

Create a new identifier with different state

Parameters:

  • new_state (Symbol)

    new piece state (:normal, :enhanced, or :diminished)

Returns:

  • (Identifier)

    new identifier with different state



250
251
252
253
254
# File 'lib/sashite/qpi/identifier.rb', line 250

def with_state(new_state)
  return self if state == new_state

  self.class.new(family, type, side, new_state)
end

#with_type(new_type) ⇒ Identifier

Create a new identifier with different piece type

Parameters:

  • new_type (Symbol)

    new piece type (:A to :Z)

Returns:

  • (Identifier)

    new identifier with different type



230
231
232
233
234
# File 'lib/sashite/qpi/identifier.rb', line 230

def with_type(new_type)
  return self if type == new_type

  self.class.new(family, new_type, side, state)
end