sashite-sin 3.0.0 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0243c39193799ab26a617517ae8deb0a63f4c6400a00ebdd265710ccae62dccb
4
- data.tar.gz: 6a7c470c3f03680ab983ebb65c66d0a28dacaf1b6f422d5a900d214999f6ba2b
3
+ metadata.gz: 611d7fa815981b0a68443f7641f2f780afe46bca6fd8fed0f6204f35b0c2721b
4
+ data.tar.gz: c414565ed46cb55375c7ec599424dea459999c2162e458ee62be81a49dcf601b
5
5
  SHA512:
6
- metadata.gz: fcd80485fabc9888e20925161a29b9c6cd1dc286d8cd802e1363e8887991226243c414c7ff6c50714b464d6eb8058519c5d04fd82458856a5c4d0a618e1185b9
7
- data.tar.gz: b1005945dd8bdcd9c818c07a104ac8582e3b2e966dc73626034cf53f7b1c7d306a59b419621c0eb3dc1705200e55ffa54904b4f54d2842348d13f797e9e3b329
6
+ metadata.gz: e8e1fa4bbc10f70ba9e17f01292228d538d639f3de6a4e1fc30207f436a1d72133edf502eef557405fce096ecb30c4c9125cd6919f3dc03cb5005e356fcb11f4
7
+ data.tar.gz: b3904a0fa64f33251e6db3cf6f454578a3dadccb75f8e3b3e885de834ecd6c9b0e0dd53aeaa62587fe9dde5979b991f0b54dc93e10b835ab9bfdad8cd31f4136
data/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2025 Cyril Kato
189
+ Copyright 2025-2026 Cyril Kato
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Version](https://img.shields.io/github/v/tag/sashite/sin.rb?label=Version&logo=github)](https://github.com/sashite/sin.rb/tags)
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/sin.rb/main)
5
- [![CI](https://github.com/sashite/sin.rb/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/sashite/sin.rb/actions)
5
+ [![CI](https://github.com/sashite/sin.rb/actions/workflows/ruby.yml/badge.svg?branch=main)](https://github.com/sashite/sin.rb/actions)
6
6
  [![License](https://img.shields.io/github/license/sashite/sin.rb?label=License&logo=github)](https://github.com/sashite/sin.rb/raw/main/LICENSE)
7
7
 
8
8
  > **SIN** (Style Identifier Notation) implementation for Ruby.
@@ -11,6 +11,18 @@
11
11
 
12
12
  This library implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/).
13
13
 
14
+ SIN is a single-character, ASCII-only token format encoding a **Player Identity**: the tuple (**Player Side**, **Player Style**). Uppercase indicates the first player, lowercase indicates the second player.
15
+
16
+ ### Implementation Constraints
17
+
18
+ | Constraint | Value | Rationale |
19
+ |------------|-------|-----------|
20
+ | Token length | Exactly 1 | Single ASCII letter per spec |
21
+ | Character space | A–Z, a–z | 52 total tokens (26 abbreviations × 2 sides) |
22
+ | Instance pool | 52 objects | All identifiers are pre-instantiated and frozen |
23
+
24
+ The closed domain of 52 possible values enables a flyweight architecture with zero allocation on the hot path.
25
+
14
26
  ## Installation
15
27
 
16
28
  ```ruby
@@ -35,69 +47,78 @@ require "sashite/sin"
35
47
 
36
48
  # Standard parsing (raises on error)
37
49
  sin = Sashite::Sin.parse("C")
38
- sin.style # => :C
39
- sin.side # => :first
50
+ sin.abbr # => :C
51
+ sin.side # => :first
40
52
 
41
53
  # Lowercase indicates second player
42
54
  sin = Sashite::Sin.parse("c")
43
- sin.style # => :C
44
- sin.side # => :second
55
+ sin.abbr # => :C
56
+ sin.side # => :second
57
+
58
+ # Returns a cached instance — no allocation
59
+ Sashite::Sin.parse("C").equal?(Sashite::Sin.parse("C")) # => true
45
60
 
46
61
  # Invalid input raises ArgumentError
47
62
  Sashite::Sin.parse("") # => raises ArgumentError
48
63
  Sashite::Sin.parse("CC") # => raises ArgumentError
49
64
  ```
50
65
 
51
- ### Formatting (IdentifierString)
66
+ ### Safe Parsing (StringIdentifier | nil)
52
67
 
53
- Convert an `Identifier` back to a SIN string.
68
+ Parse without raising exceptions. Returns `nil` on invalid input.
54
69
 
55
70
  ```ruby
56
- # From Identifier object
57
- sin = Sashite::Sin::Identifier.new(:C, :first)
58
- sin.to_s # => "C"
59
-
60
- sin = Sashite::Sin::Identifier.new(:C, :second)
61
- sin.to_s # => "c"
71
+ # Valid input returns an Identifier
72
+ Sashite::Sin.safe_parse("C") # => #<Sashite::Sin::Identifier C>
73
+ Sashite::Sin.safe_parse("c") # => #<Sashite::Sin::Identifier c>
74
+
75
+ # Invalid input returns nil — no exception allocated
76
+ Sashite::Sin.safe_parse("") # => nil
77
+ Sashite::Sin.safe_parse("CC") # => nil
78
+ Sashite::Sin.safe_parse("1") # => nil
79
+ Sashite::Sin.safe_parse(nil) # => nil
62
80
  ```
63
81
 
64
- ### Validation
82
+ ### Fetching by Components (Symbol, Symbol → Identifier)
65
83
 
66
- ```ruby
67
- # Boolean check
68
- Sashite::Sin.valid?("C") # => true
69
- Sashite::Sin.valid?("c") # => true
70
- Sashite::Sin.valid?("") # => false
71
- Sashite::Sin.valid?("CC") # => false
72
- Sashite::Sin.valid?("1") # => false
73
- ```
74
-
75
- ### Accessing Identifier Data
84
+ Retrieve a cached identifier directly by abbreviation and side, bypassing string parsing entirely.
76
85
 
77
86
  ```ruby
78
- sin = Sashite::Sin.parse("C")
87
+ # Direct lookup — no string parsing, no allocation
88
+ Sashite::Sin.fetch(:C, :first) # => #<Sashite::Sin::Identifier C>
89
+ Sashite::Sin.fetch(:C, :second) # => #<Sashite::Sin::Identifier c>
79
90
 
80
- # Get attributes
81
- sin.style # => :C
82
- sin.side # => :first
91
+ # Same cached instance as parse
92
+ Sashite::Sin.fetch(:C, :first).equal?(Sashite::Sin.parse("C")) # => true
83
93
 
84
- # Get string component
85
- sin.letter # => "C"
94
+ # Invalid components raise ArgumentError
95
+ Sashite::Sin.fetch(:CC, :first) # => raises ArgumentError
96
+ Sashite::Sin.fetch(:C, :third) # => raises ArgumentError
86
97
  ```
87
98
 
88
- ### Transformations
99
+ ### Formatting (Identifier → String)
89
100
 
90
- All transformations return new immutable `Identifier` objects.
101
+ Convert an `Identifier` back to a SIN string.
91
102
 
92
103
  ```ruby
93
104
  sin = Sashite::Sin.parse("C")
105
+ sin.to_s # => "C"
94
106
 
95
- # Side transformation
96
- sin.flip.to_s # => "c"
107
+ sin = Sashite::Sin.parse("c")
108
+ sin.to_s # => "c"
109
+ ```
97
110
 
98
- # Attribute changes
99
- sin.with_style(:S).to_s # => "S"
100
- sin.with_side(:second).to_s # => "c"
111
+ ### Validation
112
+
113
+ ```ruby
114
+ # Boolean check (never raises)
115
+ # Uses an exception-free code path internally for performance.
116
+ Sashite::Sin.valid?("C") # => true
117
+ Sashite::Sin.valid?("c") # => true
118
+ Sashite::Sin.valid?("") # => false
119
+ Sashite::Sin.valid?("CC") # => false
120
+ Sashite::Sin.valid?("1") # => false
121
+ Sashite::Sin.valid?(nil) # => false
101
122
  ```
102
123
 
103
124
  ### Queries
@@ -111,82 +132,73 @@ sin.second_player? # => false
111
132
 
112
133
  # Comparison queries
113
134
  other = Sashite::Sin.parse("c")
114
- sin.same_style?(other) # => true
115
- sin.same_side?(other) # => false
135
+ sin.same_abbr?(other) # => true
136
+ sin.same_side?(other) # => false
116
137
  ```
117
138
 
118
139
  ## API Reference
119
140
 
120
- ### Types
121
-
122
- ```ruby
123
- # Identifier represents a parsed SIN identifier with style and side.
124
- class Sashite::Sin::Identifier
125
- # Creates an Identifier from style and side.
126
- # Raises ArgumentError if attributes are invalid.
127
- #
128
- # @param style [Symbol] Style abbreviation (:A through :Z)
129
- # @param side [Symbol] Player side (:first or :second)
130
- # @return [Identifier]
131
- def initialize(style, side)
132
-
133
- # Returns the style as an uppercase symbol.
134
- #
135
- # @return [Symbol]
136
- def style
137
-
138
- # Returns the player side.
139
- #
140
- # @return [Symbol] :first or :second
141
- def side
142
-
143
- # Returns the SIN string representation.
144
- #
145
- # @return [String]
146
- def to_s
147
- end
148
- ```
149
-
150
- ### Constants
151
-
152
- ```ruby
153
- Sashite::Sin::Identifier::VALID_STYLES # => [:A, :B, ..., :Z]
154
- Sashite::Sin::Identifier::VALID_SIDES # => [:first, :second]
155
- ```
156
-
157
- ### Parsing
141
+ ### Module Methods
158
142
 
159
143
  ```ruby
160
- # Parses a SIN string into an Identifier.
144
+ # Parses a SIN string into a cached Identifier.
145
+ # Returns a pre-instantiated, frozen instance.
161
146
  # Raises ArgumentError if the string is not valid.
162
147
  #
163
148
  # @param string [String] SIN string
164
149
  # @return [Identifier]
165
150
  # @raise [ArgumentError] if invalid
166
151
  def Sashite::Sin.parse(string)
167
- ```
168
152
 
169
- ### Validation
153
+ # Parses a SIN string without raising.
154
+ # Returns a cached Identifier on success, nil on failure.
155
+ # Never allocates exception objects or captures backtraces.
156
+ #
157
+ # @param string [String] SIN string
158
+ # @return [Identifier, nil]
159
+ def Sashite::Sin.safe_parse(string)
160
+
161
+ # Retrieves a cached Identifier by abbreviation and side.
162
+ # Bypasses string parsing entirely — direct hash lookup.
163
+ # Raises ArgumentError if components are invalid.
164
+ #
165
+ # @param abbr [Symbol] Style abbreviation (:A through :Z)
166
+ # @param side [Symbol] Player side (:first or :second)
167
+ # @return [Identifier]
168
+ # @raise [ArgumentError] if invalid
169
+ def Sashite::Sin.fetch(abbr, side)
170
170
 
171
- ```ruby
172
171
  # Reports whether string is a valid SIN identifier.
172
+ # Never raises; returns false for any invalid input.
173
+ # Uses an exception-free code path internally for performance.
173
174
  #
174
175
  # @param string [String] SIN string
175
176
  # @return [Boolean]
176
177
  def Sashite::Sin.valid?(string)
177
178
  ```
178
179
 
179
- ### Transformations
180
-
181
- All transformations return new `Sashite::Sin::Identifier` objects:
180
+ ### Identifier
182
181
 
183
182
  ```ruby
184
- # Side transformation
185
- def flip # => Identifier
183
+ # Identifier represents a parsed SIN identifier with abbreviation and side.
184
+ # All instances are frozen and pre-instantiated — never construct directly,
185
+ # use Sashite::Sin.parse, .safe_parse, or .fetch instead.
186
+ class Sashite::Sin::Identifier
187
+ # Returns the style abbreviation as an uppercase symbol.
188
+ #
189
+ # @return [Symbol] :A through :Z
190
+ def abbr
191
+
192
+ # Returns the player side.
193
+ #
194
+ # @return [Symbol] :first or :second
195
+ def side
186
196
 
187
- # Attribute changes
188
- def with_style(style) # => Identifier
189
- def with_side(side) # => Identifier
197
+ # Returns the SIN string representation.
198
+ #
199
+ # @return [String]
200
+ def to_s
201
+ end
190
202
  ```
191
203
 
192
204
  ### Queries
@@ -197,13 +209,20 @@ def first_player? # => Boolean
197
209
  def second_player? # => Boolean
198
210
 
199
211
  # Comparison queries
200
- def same_style?(other) # => Boolean
201
- def same_side?(other) # => Boolean
212
+ def same_abbr?(other) # => Boolean
213
+ def same_side?(other) # => Boolean
214
+ ```
215
+
216
+ ### Constants
217
+
218
+ ```ruby
219
+ Sashite::Sin::Identifier::VALID_ABBRS # => [:A, :B, ..., :Z]
220
+ Sashite::Sin::Identifier::VALID_SIDES # => [:first, :second]
202
221
  ```
203
222
 
204
223
  ### Errors
205
224
 
206
- All parsing and validation errors raise `ArgumentError` with descriptive messages:
225
+ All errors raise `ArgumentError` with descriptive messages:
207
226
 
208
227
  | Message | Cause |
209
228
  |---------|-------|
@@ -213,12 +232,28 @@ All parsing and validation errors raise `ArgumentError` with descriptive message
213
232
 
214
233
  ## Design Principles
215
234
 
216
- - **Bounded values**: Explicit validation of styles and sides
217
- - **Object-oriented**: `Identifier` class enables methods and encapsulation
235
+ - **Spec conformance**: Strict adherence to SIN v1.0.0
236
+ - **Flyweight identifiers**: All 52 possible instances are pre-built and frozen; parsing and fetching return cached objects with zero allocation
237
+ - **Performance-oriented internals**: Exception-free validation path; exceptions only at the public API boundary
218
238
  - **Ruby idioms**: `valid?` predicate, `to_s` conversion, `ArgumentError` for invalid input
219
- - **Immutable identifiers**: All transformations return new objects
239
+ - **Immutable identifiers**: All instances are frozen after creation
220
240
  - **No dependencies**: Pure Ruby standard library only
221
241
 
242
+ ### Performance Architecture
243
+
244
+ SIN has a closed domain of exactly 52 valid tokens (26 letters × 2 cases). The implementation exploits this constraint through two complementary strategies.
245
+
246
+ **Flyweight instance pool** — All 52 `Identifier` objects are pre-instantiated and frozen at load time. `parse`, `safe_parse`, and `fetch` return these cached instances via hash lookup. No `Identifier` is ever allocated after the module loads. This makes SIN essentially free to call from FEEN or any other hot loop — every call is a hash lookup returning a pre-existing frozen object.
247
+
248
+ **Dual-path parsing** — Parsing is split into two layers to avoid using exceptions for control flow:
249
+
250
+ - **Validation layer** — `safe_parse` performs all validation and returns the cached `Identifier` on success, or `nil` on failure, without raising, without allocating exception objects, and without capturing backtraces.
251
+ - **Public API layer** — `parse` calls `safe_parse` internally. On failure, it raises `ArgumentError` exactly once, at the boundary. `valid?` calls `safe_parse` and returns a boolean directly, never raising.
252
+
253
+ **Direct component lookup** — `fetch` bypasses string parsing entirely. Given a symbol pair `(:C, :first)`, it performs a single hash lookup into the instance pool. This is the fastest path for callers that already have structured data (e.g., FEEN's dumper reconstructing a style–turn field from `Qi` attributes).
254
+
255
+ This architecture ensures that SIN never becomes a bottleneck when called from higher-level parsers like FEEN, where it may be invoked multiple times per position.
256
+
222
257
  ## Related Specifications
223
258
 
224
259
  - [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
@@ -4,35 +4,53 @@ module Sashite
4
4
  module Sin
5
5
  # Constants for the SIN (Style Identifier Notation) specification.
6
6
  #
7
- # Defines valid values for styles and sides, as well as formatting constants.
8
- #
9
- # @example Accessing valid styles
10
- # Constants::VALID_STYLES # => [:A, :B, ..., :Z]
11
- #
12
- # @example Accessing valid sides
13
- # Constants::VALID_SIDES # => [:first, :second]
7
+ # Provides public-facing domain constants and pre-computed lookup tables
8
+ # used internally for zero-allocation parsing.
14
9
  #
15
10
  # @see https://sashite.dev/specs/sin/1.0.0/
16
11
  module Constants
17
- # Valid style symbols (A-Z as uppercase symbols).
12
+ # Valid abbreviation symbols (A-Z as uppercase symbols).
18
13
  #
19
- # @return [Array<Symbol>] Array of 26 valid style symbols
20
- VALID_STYLES = %i[A B C D E F G H I J K L M N O P Q R S T U V W X Y Z].freeze
14
+ # @return [Array<Symbol>] Array of 26 valid abbreviation symbols
15
+ VALID_ABBRS = %i[A B C D E F G H I J K L M N O P Q R S T U V W X Y Z].freeze
21
16
 
22
17
  # Valid side symbols.
23
18
  #
24
19
  # @return [Array<Symbol>] Array of valid side symbols
25
20
  VALID_SIDES = %i[first second].freeze
26
21
 
27
- # Maximum length of a valid SIN string.
22
+ # Pre-computed byte uppercase Symbol lookup table.
23
+ #
24
+ # Maps every valid ASCII letter byte (A-Z, a-z) to its uppercase Symbol
25
+ # abbreviation. Used by the parser for O(1) extraction with no intermediate
26
+ # String allocation.
28
27
  #
29
- # @return [Integer] Maximum string length (1)
30
- MAX_STRING_LENGTH = 1
28
+ # @example
29
+ # BYTE_TO_ABBR[0x43] # => :C (byte for 'C')
30
+ # BYTE_TO_ABBR[0x63] # => :C (byte for 'c')
31
+ # BYTE_TO_ABBR[0x31] # => nil (byte for '1')
32
+ #
33
+ # @return [Hash{Integer => Symbol}] Frozen byte-to-symbol mapping
34
+ BYTE_TO_ABBR = {}.tap { |h|
35
+ (0x41..0x5A).each { |b| h[b] = b.chr.to_sym }
36
+ (0x61..0x7A).each { |b| h[b] = (b - 32).chr.to_sym }
37
+ }.freeze
31
38
 
32
- # Empty string constant for internal use.
39
+ # Pre-computed byte side lookup table.
40
+ #
41
+ # Maps every valid ASCII letter byte to its corresponding side.
42
+ # Uppercase letters map to :first, lowercase to :second.
43
+ #
44
+ # @example
45
+ # BYTE_TO_SIDE[0x43] # => :first (byte for 'C')
46
+ # BYTE_TO_SIDE[0x63] # => :second (byte for 'c')
47
+ # BYTE_TO_SIDE[0x31] # => nil (byte for '1')
33
48
  #
34
- # @return [String] Empty string
35
- EMPTY_STRING = ""
49
+ # @return [Hash{Integer => Symbol}] Frozen byte-to-side mapping
50
+ BYTE_TO_SIDE = {}.tap { |h|
51
+ (0x41..0x5A).each { |b| h[b] = :first }
52
+ (0x61..0x7A).each { |b| h[b] = :second }
53
+ }.freeze
36
54
  end
37
55
  end
38
56
  end
@@ -29,10 +29,10 @@ module Sashite
29
29
  # @return [String] Error message
30
30
  MUST_BE_LETTER = "must be a letter"
31
31
 
32
- # Error message for invalid style value.
32
+ # Error message for invalid abbreviation value.
33
33
  #
34
34
  # @return [String] Error message
35
- INVALID_STYLE = "invalid style"
35
+ INVALID_ABBR = "invalid abbr"
36
36
 
37
37
  # Error message for invalid side value.
38
38
  #
@@ -8,49 +8,51 @@ module Sashite
8
8
  # Represents a parsed SIN (Style Identifier Notation) identifier.
9
9
  #
10
10
  # An Identifier encodes two attributes:
11
- # - Style: the piece style (A-Z as uppercase symbol)
11
+ # - Abbr: the style abbreviation (A-Z as uppercase symbol)
12
12
  # - Side: the player side (:first or :second)
13
13
  #
14
- # Instances are immutable (frozen after creation).
14
+ # All 52 possible instances are pre-instantiated and frozen at load time.
15
+ # Never construct directly — use {Sashite::Sin.parse}, {Sashite::Sin.safe_parse},
16
+ # or {Sashite::Sin.fetch} instead.
15
17
  #
16
- # @example Creating identifiers
17
- # sin = Identifier.new(:C, :first)
18
- # sin = Identifier.new(:S, :second)
18
+ # @example Obtaining identifiers
19
+ # Sashite::Sin.parse("C") # => #<Sashite::Sin::Identifier C>
20
+ # Sashite::Sin.fetch(:C, :first) # => #<Sashite::Sin::Identifier C>
19
21
  #
20
- # @example String conversion
21
- # Identifier.new(:C, :first).to_s # => "C"
22
- # Identifier.new(:C, :second).to_s # => "c"
22
+ # @example Identity guarantee (flyweight)
23
+ # Sashite::Sin.parse("C").equal?(Sashite::Sin.fetch(:C, :first)) # => true
23
24
  #
24
25
  # @see https://sashite.dev/specs/sin/1.0.0/
25
26
  class Identifier
26
- # Valid style symbols (A-Z).
27
- VALID_STYLES = Constants::VALID_STYLES
27
+ # Valid abbreviation symbols (A-Z).
28
+ VALID_ABBRS = Constants::VALID_ABBRS
28
29
 
29
30
  # Valid side symbols.
30
31
  VALID_SIDES = Constants::VALID_SIDES
31
32
 
32
- # @return [Symbol] Piece style (:A to :Z, always uppercase)
33
- attr_reader :style
33
+ # @return [Symbol] Style abbreviation (:A to :Z, always uppercase)
34
+ attr_reader :abbr
34
35
 
35
36
  # @return [Symbol] Player side (:first or :second)
36
37
  attr_reader :side
37
38
 
38
39
  # Creates a new Identifier instance.
39
40
  #
40
- # @param style [Symbol] Piece style (:A to :Z)
41
+ # @param abbr [Symbol] Style abbreviation (:A to :Z)
41
42
  # @param side [Symbol] Player side (:first or :second)
42
43
  # @return [Identifier] A new frozen Identifier instance
43
44
  # @raise [Errors::Argument] If any attribute is invalid
44
45
  #
45
- # @example
46
- # Identifier.new(:C, :first)
47
- # Identifier.new(:S, :second)
48
- def initialize(style, side)
49
- validate_style!(style)
46
+ # @api private
47
+ def initialize(abbr, side)
48
+ validate_abbr!(abbr)
50
49
  validate_side!(side)
51
50
 
52
- @style = style
51
+ @abbr = abbr
53
52
  @side = side
53
+ @string = (side.equal?(:first) ? abbr.to_s : abbr.to_s.downcase).freeze
54
+ @hash = [abbr, side].hash
55
+ @inspect = "#<#{self.class} #{@string}>".freeze
54
56
 
55
57
  freeze
56
58
  end
@@ -61,79 +63,15 @@ module Sashite
61
63
 
62
64
  # Returns the SIN string representation.
63
65
  #
66
+ # Returns a pre-computed, frozen string — zero allocation per call.
67
+ #
64
68
  # @return [String] The single-character SIN string
65
69
  #
66
70
  # @example
67
- # Identifier.new(:C, :first).to_s # => "C"
68
- # Identifier.new(:C, :second).to_s # => "c"
71
+ # Sashite::Sin.parse("C").to_s # => "C"
72
+ # Sashite::Sin.parse("c").to_s # => "c"
69
73
  def to_s
70
- letter
71
- end
72
-
73
- # Returns the letter component of the SIN.
74
- #
75
- # @return [String] Uppercase for first player, lowercase for second
76
- #
77
- # @example
78
- # Identifier.new(:C, :first).letter # => "C"
79
- # Identifier.new(:C, :second).letter # => "c"
80
- def letter
81
- base = String(style)
82
-
83
- case side
84
- when :first then base.upcase
85
- when :second then base.downcase
86
- end
87
- end
88
-
89
- # ========================================================================
90
- # Side Transformations
91
- # ========================================================================
92
-
93
- # Returns a new Identifier with the opposite side.
94
- #
95
- # @return [Identifier] A new Identifier with flipped side
96
- #
97
- # @example
98
- # sin = Identifier.new(:C, :first)
99
- # sin.flip.to_s # => "c"
100
- def flip
101
- new_side = first_player? ? :second : :first
102
- self.class.new(style, new_side)
103
- end
104
-
105
- # ========================================================================
106
- # Attribute Transformations
107
- # ========================================================================
108
-
109
- # Returns a new Identifier with a different style.
110
- #
111
- # @param new_style [Symbol] The new piece style (:A to :Z)
112
- # @return [Identifier] A new Identifier with the specified style
113
- # @raise [Errors::Argument] If the style is invalid
114
- #
115
- # @example
116
- # sin = Identifier.new(:C, :first)
117
- # sin.with_style(:S).to_s # => "S"
118
- def with_style(new_style)
119
- return self if style.equal?(new_style)
120
-
121
- self.class.new(new_style, side)
122
- end
123
-
124
- # Returns a new Identifier with a different side.
125
- #
126
- # @param new_side [Symbol] The new side (:first or :second)
127
- # @return [Identifier] A new Identifier with the specified side
128
- # @raise [Errors::Argument] If the side is invalid
129
- #
130
- # @example
131
- # sin = Identifier.new(:C, :first)
132
- # sin.with_side(:second).to_s # => "c"
133
- def with_side(new_side)
134
- return self if side.equal?(new_side)
135
-
136
- self.class.new(style, new_side)
74
+ @string
137
75
  end
138
76
 
139
77
  # ========================================================================
@@ -145,7 +83,7 @@ module Sashite
145
83
  # @return [Boolean] true if first player
146
84
  #
147
85
  # @example
148
- # Identifier.new(:C, :first).first_player? # => true
86
+ # Sashite::Sin.parse("C").first_player? # => true
149
87
  def first_player?
150
88
  side.equal?(:first)
151
89
  end
@@ -155,7 +93,7 @@ module Sashite
155
93
  # @return [Boolean] true if second player
156
94
  #
157
95
  # @example
158
- # Identifier.new(:C, :second).second_player? # => true
96
+ # Sashite::Sin.parse("c").second_player? # => true
159
97
  def second_player?
160
98
  side.equal?(:second)
161
99
  end
@@ -164,17 +102,17 @@ module Sashite
164
102
  # Comparison Queries
165
103
  # ========================================================================
166
104
 
167
- # Checks if two Identifiers have the same style.
105
+ # Checks if two Identifiers have the same abbreviation.
168
106
  #
169
107
  # @param other [Identifier] The other Identifier to compare
170
- # @return [Boolean] true if same style
108
+ # @return [Boolean] true if same abbreviation
171
109
  #
172
110
  # @example
173
- # sin1 = Identifier.new(:C, :first)
174
- # sin2 = Identifier.new(:C, :second)
175
- # sin1.same_style?(sin2) # => true
176
- def same_style?(other)
177
- style.equal?(other.style)
111
+ # sin1 = Sashite::Sin.parse("C")
112
+ # sin2 = Sashite::Sin.parse("c")
113
+ # sin1.same_abbr?(sin2) # => true
114
+ def same_abbr?(other)
115
+ abbr.equal?(other.abbr)
178
116
  end
179
117
 
180
118
  # Checks if two Identifiers have the same side.
@@ -183,8 +121,8 @@ module Sashite
183
121
  # @return [Boolean] true if same side
184
122
  #
185
123
  # @example
186
- # sin1 = Identifier.new(:C, :first)
187
- # sin2 = Identifier.new(:S, :first)
124
+ # sin1 = Sashite::Sin.parse("C")
125
+ # sin2 = Sashite::Sin.parse("S")
188
126
  # sin1.same_side?(sin2) # => true
189
127
  def same_side?(other)
190
128
  side.equal?(other.side)
@@ -196,36 +134,36 @@ module Sashite
196
134
 
197
135
  # Checks equality with another Identifier.
198
136
  #
137
+ # With the flyweight pool, equal identifiers are always the same object
138
+ # (i.e., == implies equal?). Value-based comparison is provided for
139
+ # correctness when comparing across different object sources.
140
+ #
199
141
  # @param other [Object] The object to compare
200
142
  # @return [Boolean] true if equal
201
143
  #
202
144
  # @example
203
- # sin1 = Identifier.new(:C, :first)
204
- # sin2 = Identifier.new(:C, :first)
205
- # sin1 == sin2 # => true
145
+ # Sashite::Sin.parse("C") == Sashite::Sin.fetch(:C, :first) # => true
206
146
  def ==(other)
207
- return false unless self.class === other
208
-
209
- style.equal?(other.style) && side.equal?(other.side)
147
+ equal?(other) || (self.class === other && abbr.equal?(other.abbr) && side.equal?(other.side))
210
148
  end
211
149
 
212
150
  alias eql? ==
213
151
 
214
- # Returns a hash code for the Identifier.
152
+ # Returns a pre-computed hash code for the Identifier.
215
153
  #
216
154
  # @return [Integer] Hash code
217
155
  def hash
218
- [style, side].hash
156
+ @hash
219
157
  end
220
158
 
221
- # Returns an inspect string for the Identifier.
159
+ # Returns a pre-computed inspect string for the Identifier.
222
160
  #
223
161
  # @return [String] Inspect representation
224
162
  #
225
163
  # @example
226
- # Identifier.new(:C, :first).inspect # => "#<Sashite::Sin::Identifier C>"
164
+ # Sashite::Sin.parse("C").inspect # => "#<Sashite::Sin::Identifier C>"
227
165
  def inspect
228
- "#<#{self.class} #{self}>"
166
+ @inspect
229
167
  end
230
168
 
231
169
  private
@@ -234,10 +172,10 @@ module Sashite
234
172
  # Private Validation
235
173
  # ========================================================================
236
174
 
237
- def validate_style!(style)
238
- return if ::Symbol === style && Constants::VALID_STYLES.include?(style)
175
+ def validate_abbr!(abbr)
176
+ return if ::Symbol === abbr && Constants::VALID_ABBRS.include?(abbr)
239
177
 
240
- raise Errors::Argument, Errors::Argument::Messages::INVALID_STYLE
178
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_ABBR
241
179
  end
242
180
 
243
181
  def validate_side!(side)
@@ -245,6 +183,38 @@ module Sashite
245
183
 
246
184
  raise Errors::Argument, Errors::Argument::Messages::INVALID_SIDE
247
185
  end
186
+
187
+ # ========================================================================
188
+ # Flyweight Instance Pool
189
+ # ========================================================================
190
+
191
+ public
192
+
193
+ # Component-keyed pool: [abbr, side] → Identifier.
194
+ #
195
+ # Used by {Sashite::Sin.fetch} for direct lookup by structured components.
196
+ #
197
+ # @return [Hash{Array(Symbol, Symbol) => Identifier}]
198
+ POOL = {}.tap { |pool|
199
+ Constants::VALID_ABBRS.each do |abbr|
200
+ Constants::VALID_SIDES.each do |side|
201
+ pool[[abbr, side]] = new(abbr, side)
202
+ end
203
+ end
204
+ }.freeze
205
+
206
+ # Byte-keyed pool: ASCII byte → Identifier.
207
+ #
208
+ # Used by the parser for O(1) lookup from a validated byte.
209
+ # Returns nil for non-letter bytes, doubling as implicit validation.
210
+ #
211
+ # @return [Hash{Integer => Identifier}]
212
+ BYTE_POOL = {}.tap { |pool|
213
+ (0x41..0x5A).each { |b| pool[b] = POOL[[b.chr.to_sym, :first]] }
214
+ (0x61..0x7A).each { |b| pool[b] = POOL[[(b - 32).chr.to_sym, :second]] }
215
+ }.freeze
216
+
217
+ private_class_method :new
248
218
  end
249
219
  end
250
220
  end
@@ -1,18 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "constants"
4
3
  require_relative "errors"
4
+ require_relative "identifier"
5
5
 
6
6
  module Sashite
7
7
  module Sin
8
8
  # Parses SIN (Style Identifier Notation) strings.
9
9
  #
10
- # The parser uses byte-level validation to ensure security against
11
- # malformed input, Unicode lookalikes, and injection attacks.
10
+ # Implements a dual-path architecture for maximum performance:
11
+ #
12
+ # - {.safe_parse} validates and returns a cached Identifier or nil,
13
+ # without ever raising or allocating exception objects.
14
+ # - {.parse} delegates to safe_parse and raises exactly once at the
15
+ # API boundary on failure.
16
+ # - {.valid?} delegates to safe_parse and converts to boolean.
17
+ #
18
+ # All valid inputs resolve to a pre-instantiated flyweight Identifier
19
+ # via a single byte-indexed Hash lookup.
12
20
  #
13
21
  # @example Parsing a valid SIN string
14
- # Parser.parse("C") # => { style: :C, side: :first }
15
- # Parser.parse("c") # => { style: :C, side: :second }
22
+ # Parser.parse("C") # => #<Sashite::Sin::Identifier C>
23
+ # Parser.parse("c") # => #<Sashite::Sin::Identifier c>
24
+ #
25
+ # @example Safe parsing
26
+ # Parser.safe_parse("C") # => #<Sashite::Sin::Identifier C>
27
+ # Parser.safe_parse("1") # => nil
28
+ # Parser.safe_parse(nil) # => nil
16
29
  #
17
30
  # @example Validation
18
31
  # Parser.valid?("C") # => true
@@ -20,28 +33,45 @@ module Sashite
20
33
  #
21
34
  # @see https://sashite.dev/specs/sin/1.0.0/
22
35
  module Parser
23
- # Parses a SIN string into its components.
36
+ # Parses a SIN string into a cached Identifier.
37
+ #
38
+ # Delegates to {.safe_parse} internally. On failure, raises a single
39
+ # ArgumentError with a descriptive message.
24
40
  #
25
41
  # @param input [String] The SIN string to parse
26
- # @return [Hash] Hash with :style and :side keys
42
+ # @return [Identifier] A pre-instantiated, frozen Identifier
27
43
  # @raise [Errors::Argument] If the input is invalid
28
44
  #
29
45
  # @example
30
- # Parser.parse("C") # => { style: :C, side: :first }
31
- # Parser.parse("s") # => { style: :S, side: :second }
46
+ # Parser.parse("C") # => #<Sashite::Sin::Identifier C>
47
+ # Parser.parse("s") # => #<Sashite::Sin::Identifier s>
32
48
  def self.parse(input)
33
- validate_input_type!(input)
34
- validate_not_empty!(input)
35
- validate_length!(input)
36
-
37
- byte = input.getbyte(0)
38
- validate_letter!(byte)
49
+ safe_parse(input) || raise(Errors::Argument, error_message_for(input))
50
+ end
39
51
 
40
- extract_components(byte)
52
+ # Parses a SIN string without raising.
53
+ #
54
+ # Returns a cached Identifier on success, nil on failure.
55
+ # Never allocates exception objects or captures backtraces.
56
+ #
57
+ # @param input [String] The string to parse
58
+ # @return [Identifier, nil] A cached Identifier, or nil if invalid
59
+ #
60
+ # @example
61
+ # Parser.safe_parse("C") # => #<Sashite::Sin::Identifier C>
62
+ # Parser.safe_parse("") # => nil
63
+ # Parser.safe_parse(nil) # => nil
64
+ def self.safe_parse(input)
65
+ return unless ::String === input
66
+ return unless input.bytesize == 1
67
+
68
+ Identifier::BYTE_POOL[input.getbyte(0)]
41
69
  end
42
70
 
43
71
  # Reports whether the input is a valid SIN string.
44
72
  #
73
+ # Never raises; returns false for any invalid input including non-String.
74
+ #
45
75
  # @param input [String] The string to validate
46
76
  # @return [Boolean] true if valid, false otherwise
47
77
  #
@@ -51,87 +81,20 @@ module Sashite
51
81
  # Parser.valid?("") # => false
52
82
  # Parser.valid?("CC") # => false
53
83
  def self.valid?(input)
54
- parse(input)
55
- true
56
- rescue Errors::Argument
57
- false
84
+ !safe_parse(input).nil?
58
85
  end
59
86
 
60
- # @!group Private Class Methods
61
-
62
- # Validates that input is a String.
87
+ # Determines the appropriate error message for an invalid input.
63
88
  #
64
- # @param input [Object] The input to validate
65
- # @raise [Errors::Argument] If input is not a String
66
- # @return [void]
67
- private_class_method def self.validate_input_type!(input)
68
- return if ::String === input
69
-
70
- raise Errors::Argument, Errors::Argument::Messages::MUST_BE_LETTER
89
+ # @param input [Object] The invalid input
90
+ # @return [String] A descriptive error message
91
+ private_class_method def self.error_message_for(input)
92
+ return Errors::Argument::Messages::MUST_BE_LETTER unless ::String === input
93
+ return Errors::Argument::Messages::EMPTY_INPUT if input.empty?
94
+ return Errors::Argument::Messages::INPUT_TOO_LONG if input.bytesize > 1
95
+
96
+ Errors::Argument::Messages::MUST_BE_LETTER
71
97
  end
72
-
73
- # Validates that input is not empty.
74
- #
75
- # @param input [String] The input to validate
76
- # @raise [Errors::Argument] If input is empty
77
- # @return [void]
78
- private_class_method def self.validate_not_empty!(input)
79
- return unless input.empty?
80
-
81
- raise Errors::Argument, Errors::Argument::Messages::EMPTY_INPUT
82
- end
83
-
84
- # Validates that input does not exceed maximum length.
85
- #
86
- # @param input [String] The input to validate
87
- # @raise [Errors::Argument] If input exceeds maximum length
88
- # @return [void]
89
- private_class_method def self.validate_length!(input)
90
- return if input.bytesize <= Constants::MAX_STRING_LENGTH
91
-
92
- raise Errors::Argument, Errors::Argument::Messages::INPUT_TOO_LONG
93
- end
94
-
95
- # Validates that byte is an ASCII letter.
96
- #
97
- # @param byte [Integer] The byte to validate
98
- # @raise [Errors::Argument] If byte is not a letter
99
- # @return [void]
100
- private_class_method def self.validate_letter!(byte)
101
- return if uppercase_letter?(byte) || lowercase_letter?(byte)
102
-
103
- raise Errors::Argument, Errors::Argument::Messages::MUST_BE_LETTER
104
- end
105
-
106
- # Extracts style and side from a validated byte.
107
- #
108
- # @param byte [Integer] A validated ASCII letter byte
109
- # @return [Hash] Hash with :style and :side keys
110
- private_class_method def self.extract_components(byte)
111
- if uppercase_letter?(byte)
112
- { style: byte.chr.to_sym, side: :first }
113
- else
114
- { style: byte.chr.upcase.to_sym, side: :second }
115
- end
116
- end
117
-
118
- # Reports whether byte is an uppercase ASCII letter (A-Z).
119
- #
120
- # @param byte [Integer] The byte to check
121
- # @return [Boolean] true if A-Z
122
- private_class_method def self.uppercase_letter?(byte)
123
- byte >= 0x41 && byte <= 0x5A
124
- end
125
-
126
- # Reports whether byte is a lowercase ASCII letter (a-z).
127
- #
128
- # @param byte [Integer] The byte to check
129
- # @return [Boolean] true if a-z
130
- private_class_method def self.lowercase_letter?(byte)
131
- byte >= 0x61 && byte <= 0x7A
132
- end
133
-
134
- # @!endgroup
135
98
  end
136
99
  end
137
100
  end
data/lib/sashite/sin.rb CHANGED
@@ -15,46 +15,101 @@ module Sashite
15
15
  # - Uppercase (A-Z) indicates first player
16
16
  # - Lowercase (a-z) indicates second player
17
17
  #
18
+ # All 52 valid identifiers are pre-instantiated and frozen at load time.
19
+ # Every public method returns a cached instance — zero allocation on the
20
+ # hot path.
21
+ #
18
22
  # @example Parsing SIN strings
19
23
  # sin = Sashite::Sin.parse("C")
20
- # sin.style # => :C
21
- # sin.side # => :first
22
- # sin.to_s # => "C"
23
- #
24
- # @example Creating identifiers directly
25
- # sin = Sashite::Sin::Identifier.new(:C, :first)
24
+ # sin.abbr # => :C
25
+ # sin.side # => :first
26
26
  # sin.to_s # => "C"
27
27
  #
28
+ # @example Safe parsing (no exceptions)
29
+ # Sashite::Sin.safe_parse("C") # => #<Sashite::Sin::Identifier C>
30
+ # Sashite::Sin.safe_parse("1") # => nil
31
+ # Sashite::Sin.safe_parse(nil) # => nil
32
+ #
33
+ # @example Direct component lookup (fastest path)
34
+ # Sashite::Sin.fetch(:C, :first) # => #<Sashite::Sin::Identifier C>
35
+ #
36
+ # @example Identity guarantee (flyweight)
37
+ # Sashite::Sin.parse("C").equal?(Sashite::Sin.fetch(:C, :first)) # => true
38
+ #
28
39
  # @example Validation
29
40
  # Sashite::Sin.valid?("C") # => true
30
41
  # Sashite::Sin.valid?("CC") # => false
31
42
  #
32
43
  # @see https://sashite.dev/specs/sin/1.0.0/
33
44
  module Sin
34
- # Parses a SIN string into an Identifier.
45
+ # Parses a SIN string into a cached Identifier.
35
46
  #
36
- # @param input [String] The SIN string to parse
37
- # @return [Identifier] The parsed identifier
47
+ # Returns a pre-instantiated, frozen instance.
48
+ # Raises ArgumentError if the string is not valid.
49
+ #
50
+ # @param input [String] SIN string
51
+ # @return [Identifier] A cached, frozen Identifier
38
52
  # @raise [Errors::Argument] If the input is invalid
39
53
  #
40
54
  # @example Parsing uppercase (first player)
41
55
  # sin = Sashite::Sin.parse("C")
42
- # sin.style # => :C
43
- # sin.side # => :first
56
+ # sin.abbr # => :C
57
+ # sin.side # => :first
44
58
  #
45
59
  # @example Parsing lowercase (second player)
46
60
  # sin = Sashite::Sin.parse("c")
47
- # sin.style # => :C
48
- # sin.side # => :second
61
+ # sin.abbr # => :C
62
+ # sin.side # => :second
49
63
  def self.parse(input)
50
- components = Parser.parse(input)
64
+ Parser.parse(input)
65
+ end
66
+
67
+ # Parses a SIN string without raising.
68
+ #
69
+ # Returns a cached Identifier on success, nil on failure.
70
+ # Never allocates exception objects or captures backtraces.
71
+ #
72
+ # @param input [String] SIN string
73
+ # @return [Identifier, nil] A cached Identifier, or nil if invalid
74
+ #
75
+ # @example
76
+ # Sashite::Sin.safe_parse("C") # => #<Sashite::Sin::Identifier C>
77
+ # Sashite::Sin.safe_parse("") # => nil
78
+ # Sashite::Sin.safe_parse(nil) # => nil
79
+ def self.safe_parse(input)
80
+ Parser.safe_parse(input)
81
+ end
51
82
 
52
- Identifier.new(components.fetch(:style), components.fetch(:side))
83
+ # Retrieves a cached Identifier by abbreviation and side.
84
+ #
85
+ # Bypasses string parsing entirely — direct hash lookup into the
86
+ # flyweight pool. This is the fastest path for callers that already
87
+ # have structured data.
88
+ #
89
+ # @param abbr [Symbol] Style abbreviation (:A through :Z)
90
+ # @param side [Symbol] Player side (:first or :second)
91
+ # @return [Identifier] A cached, frozen Identifier
92
+ # @raise [Errors::Argument] If components are invalid
93
+ #
94
+ # @example
95
+ # Sashite::Sin.fetch(:C, :first) # => #<Sashite::Sin::Identifier C>
96
+ # Sashite::Sin.fetch(:C, :second) # => #<Sashite::Sin::Identifier c>
97
+ def self.fetch(abbr, side)
98
+ Identifier::POOL.fetch([abbr, side]) do
99
+ if !Identifier::VALID_ABBRS.include?(abbr)
100
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_ABBR
101
+ else
102
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_SIDE
103
+ end
104
+ end
53
105
  end
54
106
 
55
- # Reports whether the input is a valid SIN string.
107
+ # Reports whether string is a valid SIN identifier.
108
+ #
109
+ # Never raises; returns false for any invalid input including non-String.
110
+ # Uses an exception-free code path internally for performance.
56
111
  #
57
- # @param input [String] The string to validate
112
+ # @param input [String] SIN string
58
113
  # @return [Boolean] true if valid, false otherwise
59
114
  #
60
115
  # @example
@@ -63,6 +118,7 @@ module Sashite
63
118
  # Sashite::Sin.valid?("") # => false
64
119
  # Sashite::Sin.valid?("CC") # => false
65
120
  # Sashite::Sin.valid?("1") # => false
121
+ # Sashite::Sin.valid?(nil) # => false
66
122
  def self.valid?(input)
67
123
  Parser.valid?(input)
68
124
  end
data/lib/sashite-sin.rb CHANGED
@@ -1,14 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "sashite/sin"
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 Sashité Protocol specifications.
9
- #
10
- # @see https://sashite.dev/protocol/ Sashité Protocol
11
- # @see https://sashite.dev/specs/ Sashité Specifications
12
- # @author Sashité
13
- module Sashite
14
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-sin
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -53,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  requirements: []
56
- rubygems_version: 4.0.3
56
+ rubygems_version: 4.0.5
57
57
  specification_version: 4
58
58
  summary: SIN (Style Identifier Notation) implementation for Ruby with immutable identifier
59
59
  objects