qpi.ex

Hex.pmDocsLicense

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:

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"}
  ]
end

Dependencies

{:sashite_sin, "~> 2.1"}  # Style Identifier Notation
{:sashite_pin, "~> 2.1"}  # Piece Identifier Notation

Usage

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 ArgumentError

Formatting (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")       # => false

Accessing 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 Styleqpi.sin.abbr SIN letter (case-insensitive identity)
Piece Nameqpi.pin.abbr PIN letter (case-insensitive identity)
Piece Sideqpi.pin.side PIN letter case (uppercase = first, lowercase = second)
Piece Stateqpi.pin.state PIN modifier (+ = enhanced, - = diminished)
Terminal Statusqpi.pin.terminal PIN marker (^ = terminal)

Additionally, QPI provides a Native/Derived relationship via native?/1, derived?/1, native/1, and derive/1.

Design Principles

Related Specifications

License

Available as open source under the Apache License 2.0.