qpi.ex
QPI (Qualified Piece Identifier) implementation for Elixir.
Overview
This library implements the QPI Specification v1.0.0.
QPI provides complete piece identification by combining two primitive notations:
- SIN (Style Identifier Notation) — identifies the piece style
- PIN (Piece Identifier Notation) — identifies the piece attributes
A QPI identifier is a pair of (SIN, PIN) that encodes complete Piece Identity.
Installation
Add sashite_qpi to your list of dependencies in mix.exs:
def deps do
[
{:sashite_qpi, "~> 1.1"}
]
endDependencies
{:sashite_sin, "~> 2.1"} # Style Identifier Notation
{:sashite_pin, "~> 2.1"} # Piece Identifier NotationUsage
Parsing (String → Identifier)
Convert a QPI string into an Identifier struct.
# Standard parsing (returns {:ok, identifier} or {:error, reason})
{:ok, qpi} = Sashite.Qpi.parse("C:K^")
qpi.sin.abbr # => :C (Piece Style)
qpi.pin.abbr # => :K (Piece Name)
qpi.pin.side # => :first (Piece Side)
qpi.pin.state # => :normal (Piece State)
qpi.pin.terminal # => true (Terminal Status)
# Components are full SIN and PIN structs
Sashite.Sin.Identifier.first_player?(qpi.sin) # => true
Sashite.Pin.Identifier.enhanced?(qpi.pin) # => false
# Bang version (raises on error)
qpi = Sashite.Qpi.parse!("C:K^")
# Invalid input
{:error, :empty_input} = Sashite.Qpi.parse("")
{:error, :missing_separator} = Sashite.Qpi.parse("CK")
Sashite.Qpi.parse!("invalid") # => raises ArgumentErrorFormatting (Identifier → String)
Convert an Identifier back to a QPI string.
alias Sashite.Qpi.Identifier
# From components
sin = Sashite.Sin.parse!("C")
pin = Sashite.Pin.parse!("K^")
qpi = Identifier.new(sin, pin)
Identifier.to_string(qpi) # => "C:K^"
# With attributes
sin = Sashite.Sin.parse!("s")
pin = Sashite.Pin.parse!("+r")
qpi = Identifier.new(sin, pin)
Identifier.to_string(qpi) # => "s:+r"Validation
# Boolean check
Sashite.Qpi.valid?("C:K^") # => true
Sashite.Qpi.valid?("s:+r") # => true
Sashite.Qpi.valid?("invalid") # => false
Sashite.Qpi.valid?("C:") # => false
Sashite.Qpi.valid?(":K") # => falseAccessing Components
{:ok, qpi} = Sashite.Qpi.parse("S:+R^")
# Get components (struct fields)
qpi.sin # => %Sashite.Sin.Identifier{abbr: :S, side: :first}
qpi.pin # => %Sashite.Pin.Identifier{abbr: :R, side: :first, state: :enhanced, terminal: true}
# Serialize components
Sashite.Sin.Identifier.to_string(qpi.sin) # => "S"
Sashite.Pin.Identifier.to_string(qpi.pin) # => "+R^"
Sashite.Qpi.Identifier.to_string(qpi) # => "S:+R^"Five Piece Identity Attributes
All attributes come directly from the components:
{:ok, qpi} = Sashite.Qpi.parse("S:+R^")
# From SIN component
qpi.sin.abbr # => :S (Piece Style)
# From PIN component
qpi.pin.abbr # => :R (Piece Name)
qpi.pin.side # => :first (Piece Side)
qpi.pin.state # => :enhanced (Piece State)
qpi.pin.terminal # => true (Terminal Status)Native and Derived Relationship
QPI defines a deterministic relationship based on case comparison between SIN and PIN letters.
alias Sashite.Qpi.Identifier
{:ok, qpi} = Sashite.Qpi.parse("C:K^")
# Access the relationship
qpi.sin.side # => :first (derived from SIN letter case)
Identifier.native?(qpi) # => true (sin.side == pin.side)
Identifier.derived?(qpi) # => false
# Native: SIN case matches PIN case
Sashite.Qpi.parse!("C:K") |> Identifier.native?() # => true (both uppercase/first)
Sashite.Qpi.parse!("c:k") |> Identifier.native?() # => true (both lowercase/second)
# Derived: SIN case differs from PIN case
Sashite.Qpi.parse!("C:k") |> Identifier.derived?() # => true (uppercase vs lowercase)
Sashite.Qpi.parse!("c:K") |> Identifier.derived?() # => true (lowercase vs uppercase)Transformations
All transformations return new immutable structs.
alias Sashite.Qpi.Identifier
qpi = Sashite.Qpi.parse!("C:K^")
# Replace SIN component
new_sin = Sashite.Sin.parse!("S")
Identifier.with_sin(qpi, new_sin) |> Identifier.to_string() # => "S:K^"
# Replace PIN component
new_pin = Sashite.Pin.parse!("+Q^")
Identifier.with_pin(qpi, new_pin) |> Identifier.to_string() # => "C:+Q^"
# Flip PIN side (SIN unchanged)
Identifier.flip(qpi) |> Identifier.to_string() # => "C:k^"
# Native/Derived transformations (modify PIN only)
qpi = Sashite.Qpi.parse!("C:r")
Identifier.native(qpi) |> Identifier.to_string() # => "C:R" (PIN case aligned with SIN case)
Identifier.derive(qpi) |> Identifier.to_string() # => "C:r" (already derived, unchanged)
qpi = Sashite.Qpi.parse!("C:R")
Identifier.native(qpi) |> Identifier.to_string() # => "C:R" (already native, unchanged)
Identifier.derive(qpi) |> Identifier.to_string() # => "C:r" (PIN case differs from SIN case)Transform via Components
alias Sashite.Qpi.Identifier
alias Sashite.Sin.Identifier, as: SinId
alias Sashite.Pin.Identifier, as: PinId
qpi = Sashite.Qpi.parse!("C:K^")
# Transform PIN via component
Identifier.with_pin(qpi, PinId.with_abbr(qpi.pin, :Q)) |> Identifier.to_string() # => "C:Q^"
Identifier.with_pin(qpi, PinId.with_state(qpi.pin, :enhanced)) |> Identifier.to_string() # => "C:+K^"
Identifier.with_pin(qpi, PinId.with_terminal(qpi.pin, false)) |> Identifier.to_string() # => "C:K"
# Replace SIN with new instance
Identifier.with_sin(qpi, Sashite.Sin.parse!("S")) |> Identifier.to_string() # => "S:K^"
# Chain transformations
qpi |> Identifier.flip() |> Identifier.with_sin(Sashite.Sin.parse!("c")) |> Identifier.to_string() # => "c:k^"Component Queries
Since QPI is a composition, use the component APIs directly:
alias Sashite.Sin.Identifier, as: SinId
alias Sashite.Pin.Identifier, as: PinId
{:ok, qpi} = Sashite.Qpi.parse("S:+P^")
# SIN queries (style and side)
qpi.sin.abbr # => :S
qpi.sin.side # => :first
SinId.first_player?(qpi.sin) # => true
SinId.to_string(qpi.sin) # => "S"
# PIN queries (abbr, state, terminal)
qpi.pin.abbr # => :P
qpi.pin.state # => :enhanced
qpi.pin.terminal # => true
PinId.enhanced?(qpi.pin) # => true
PinId.letter(qpi.pin) # => "P"
PinId.prefix(qpi.pin) # => "+"
PinId.suffix(qpi.pin) # => "^"
# Compare QPIs via components
{:ok, other} = Sashite.Qpi.parse("C:+P^")
SinId.same_abbr?(qpi.sin, other.sin) # => false (S vs C)
PinId.same_abbr?(qpi.pin, other.pin) # => true (both P)
SinId.same_side?(qpi.sin, other.sin) # => true (both first)
PinId.same_state?(qpi.pin, other.pin) # => true (both enhanced)API Reference
Types
# Identifier represents a parsed QPI with complete Piece Identity.
%Sashite.Qpi.Identifier{
sin: %Sashite.Sin.Identifier{}, # SIN component
pin: %Sashite.Pin.Identifier{} # PIN component
}
# Create an Identifier from SIN and PIN components.
# Raises ArgumentError if components are invalid.
@spec Sashite.Qpi.Identifier.new(Sin.Identifier.t(), Pin.Identifier.t()) :: t()Parsing
# Parses a QPI string into an Identifier.
# Returns {:ok, identifier} or {:error, reason}.
@spec Sashite.Qpi.parse(String.t()) :: {:ok, Identifier.t()} | {:error, atom()}
# Parses a QPI string into an Identifier.
# Raises ArgumentError if the string is not valid.
@spec Sashite.Qpi.parse!(String.t()) :: Identifier.t()Validation
# Reports whether string is a valid QPI.
@spec Sashite.Qpi.valid?(term()) :: boolean()Transformations
All transformations return new %Sashite.Qpi.Identifier{} structs:
# Component replacement
@spec with_sin(t(), Sin.Identifier.t()) :: t()
@spec with_pin(t(), Pin.Identifier.t()) :: t()
# Flip transformation (modifies PIN only)
@spec flip(t()) :: t()
# Native/Derived transformations (modify PIN only)
@spec native(t()) :: t()
@spec derive(t()) :: t()Queries
# Native/Derived queries
@spec native?(t()) :: boolean()
@spec derived?(t()) :: boolean()Errors
Parsing returns {:error, reason} tuples with these atoms:
| Reason | Cause |
|---|---|
:empty_input | String length is 0 |
:missing_separator |
No : found in string |
:missing_sin |
Nothing before : |
:missing_pin |
Nothing after : |
:invalid_sin | SIN parsing failed |
:invalid_pin | PIN parsing failed |
Piece Identity Mapping
QPI encodes complete Piece Identity as defined in the Glossary:
| Piece Attribute | QPI Access | Encoding |
|---|---|---|
| Piece Style | qpi.sin.abbr | SIN letter (case-insensitive identity) |
| Piece Name | qpi.pin.abbr | PIN letter (case-insensitive identity) |
| Piece Side | qpi.pin.side | PIN letter case (uppercase = first, lowercase = second) |
| Piece State | qpi.pin.state |
PIN modifier (+ = enhanced, - = diminished) |
| Terminal Status | qpi.pin.terminal |
PIN marker (^ = terminal) |
Additionally, QPI provides a Native/Derived relationship via native?/1, derived?/1, native/1, and derive/1.
Design Principles
- Pure composition: QPI composes SIN and PIN without reimplementing features
- Minimal API: Core functions (
native?,derived?,native,derive,to_string) plus transformations - Component transparency: Access components directly via struct fields
- QPI-specific conveniences:
flip/1,native/1,derive/1(operations that modify PIN only, per spec) - Functional style: Pure functions, immutable structs
- Elixir idioms:
{:ok, _}/{:error, _}tuples,parse!bang variant - Pipe-friendly: Transformations designed for
|>operator - No duplication: Delegates to
sashite_sinandsashite_pin
Related Specifications
- Game Protocol — Conceptual foundation
- QPI Specification — Official specification
- QPI Examples — Usage examples
- SIN Specification — Style component
- PIN Specification — Piece component
License
Available as open source under the Apache License 2.0.