epin.ex

Hex VersionHex DocsCILicense

EPIN (Extended Piece Identifier Notation) implementation for Elixir.

Overview

This library implements the EPIN Specification v1.0.0.

EPIN extends PIN with an optional derivation marker (') that flags whether a piece uses a native or derived style.

Installation

Add to your mix.exs:

def deps do
  [
    {:sashite_epin, "~> 1.2"}
  ]
end

Dependencies

{:sashite_pin, "~> 3.1"}  # Piece Identifier Notation

Usage

Parsing (String → Identifier)

Convert an EPIN string into an Identifier struct.

# Safe parsing (returns {:ok, identifier} or {:error, reason})
{:ok, epin} = Sashite.Epin.parse("K^'")
Sashite.Epin.Identifier.to_string(epin)  # => "K^'"

# Access PIN attributes through the component
epin.pin.abbr      # => :K
epin.pin.side      # => :first
epin.pin.state     # => :normal
epin.pin.terminal  # => true

# Access derivation status
Sashite.Epin.Identifier.derived?(epin)  # => true
Sashite.Epin.Identifier.native?(epin)   # => false

# PIN component is a full Sashite.Pin.Identifier struct
Sashite.Pin.Identifier.enhanced?(epin.pin)      # => false
Sashite.Pin.Identifier.first_player?(epin.pin)  # => true

# Bang variant raises on error
epin = Sashite.Epin.parse!("K^'")

# Invalid input
{:error, reason} = Sashite.Epin.parse("invalid")

Formatting (Identifier → String)

Convert an Identifier back to an EPIN string.

alias Sashite.Epin.Identifier
alias Sashite.Pin.Identifier, as: PinId

# From PIN component
{:ok, pin} = Sashite.Pin.parse("K^")
epin = Identifier.new(pin)
Identifier.to_string(epin)  # => "K^"

# With derivation
epin = Identifier.new(pin, derived: true)
Identifier.to_string(epin)  # => "K^'"

# String interpolation works via String.Chars protocol
"Piece: #{epin}"  # => "Piece: K^'"

# Developer-friendly inspection via Inspect protocol
inspect(epin)  # => "#Sashite.Epin.Identifier<K^&#39;>"

Validation

# Boolean check
Sashite.Epin.valid?("K")         # => true
Sashite.Epin.valid?("+R^&#39;")      # => true
Sashite.Epin.valid?("invalid")   # => false
Sashite.Epin.valid?("K&#39;&#39;")       # => false
Sashite.Epin.valid?("K&#39;^")       # => false

Accessing Components

{:ok, epin} = Sashite.Epin.parse("+R^&#39;")

# Get PIN component
epin.pin  # => %Sashite.Pin.Identifier{abbr: :R, side: :first, state: :enhanced, terminal: true}
Sashite.Pin.Identifier.to_string(epin.pin)  # => "+R^"

# Check derivation
Sashite.Epin.Identifier.derived?(epin)  # => true
Sashite.Epin.Identifier.native?(epin)   # => false

# Serialize
Sashite.Epin.Identifier.to_string(epin)  # => "+R^&#39;"

Transformations

All transformations return new immutable structs.

alias Sashite.Epin.Identifier

{:ok, epin} = Sashite.Epin.parse("K^")

# Derivation transformations
Identifier.to_string(Identifier.derive(epin))  # => "K^&#39;"
Identifier.to_string(Identifier.native(epin))  # => "K^"

# Replace PIN component
{:ok, new_pin} = Sashite.Pin.parse("+Q^")
Identifier.to_string(Identifier.with_pin(epin, new_pin))  # => "+Q^"

Transform via PIN Component

alias Sashite.Epin.Identifier
alias Sashite.Pin.Identifier, as: PinId

{:ok, epin} = Sashite.Epin.parse("K^&#39;")

# Change abbr
epin
|> Identifier.with_pin(PinId.with_abbr(epin.pin, :Q))
|> Identifier.to_string()  # => "Q^&#39;"

# Change state
epin
|> Identifier.with_pin(PinId.enhance(epin.pin))
|> Identifier.to_string()  # => "+K^&#39;"

# Change side
epin
|> Identifier.with_pin(PinId.flip(epin.pin))
|> Identifier.to_string()  # => "k^&#39;"

# Remove terminal
epin
|> Identifier.with_pin(PinId.non_terminal(epin.pin))
|> Identifier.to_string()  # => "K&#39;"

Component Queries

Use the PIN API directly:

alias Sashite.Epin.Identifier
alias Sashite.Pin.Identifier, as: PinId

{:ok, epin} = Sashite.Epin.parse("+P^&#39;")

# PIN queries
epin.pin.abbr                   # => :P
epin.pin.side                   # => :first
epin.pin.state                  # => :enhanced
epin.pin.terminal               # => true
PinId.first_player?(epin.pin)   # => true
PinId.enhanced?(epin.pin)       # => true

# EPIN queries
Identifier.derived?(epin)  # => true
Identifier.native?(epin)   # => false

# Compare EPINs
{:ok, other} = Sashite.Epin.parse("+P^")
PinId.same_abbr?(epin.pin, other.pin)    # => true
PinId.same_state?(epin.pin, other.pin)   # => true
Identifier.same_derived?(epin, other)    # => false

API Reference

Types

# Identifier represents a parsed EPIN combining PIN with derivation status.
defmodule Sashite.Epin.Identifier do
  @type t :: %__MODULE__{
    pin: Sashite.Pin.Identifier.t(),
    derived: boolean()
  }

  # Creates an Identifier from a PIN component.
  # Raises ArgumentError if the PIN is invalid.
  @spec new(Sashite.Pin.Identifier.t(), keyword()) :: t()
  def new(pin, opts \\ [])

  # Returns true if derived style.
  @spec derived?(t()) :: boolean()
  def derived?(identifier)

  # Returns true if native style.
  @spec native?(t()) :: boolean()
  def native?(identifier)

  # Returns the EPIN string representation.
  @spec to_string(t()) :: String.t()
  def to_string(identifier)
end

Parsing

# Parses an EPIN string into an Identifier.
@spec Sashite.Epin.parse(String.t()) :: {:ok, Identifier.t()} | {:error, atom()}
def parse(string)

# Parses an EPIN string, raises on error.
@spec Sashite.Epin.parse!(String.t()) :: Identifier.t()
def parse!(string)

Validation

# Reports whether string is a valid EPIN.
@spec Sashite.Epin.valid?(term()) :: boolean()
def valid?(string)

Transformations

# PIN replacement (returns new Identifier)
@spec with_pin(t(), Sashite.Pin.Identifier.t()) :: t()
def with_pin(identifier, new_pin)

# Derivation transformations
@spec derive(t()) :: t()
def derive(identifier)

@spec native(t()) :: t()
def native(identifier)

Errors

Parsing errors return tagged tuples:

Error Cause
{:error, :not_a_string} Input is not a binary
{:error, :empty_input} Empty string
{:error, :invalid_derivation_marker} Derivation marker misplaced or duplicated
{:error, :invalid_pin} PIN parsing failed (or input exceeds max length)

The bang variant parse!/1 raises ArgumentError with descriptive messages.

PIN Compatibility

Every valid PIN is a valid EPIN (native by default):

~w[K +R -p K^ +R^]
|> Enum.each(fn pin_token ->
  {:ok, epin} = Sashite.Epin.parse(pin_token)
  true = Sashite.Epin.Identifier.native?(epin)
  ^pin_token = Sashite.Epin.Identifier.to_string(epin)
end)

Design Principles

Related Specifications

License

Available as open source under the Apache License 2.0.