sashite-sin 3.1.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 +4 -4
- data/README.md +123 -47
- data/lib/sashite/sin/constants.rb +31 -13
- data/lib/sashite/sin/identifier.rb +66 -35
- data/lib/sashite/sin/parser.rb +56 -93
- data/lib/sashite/sin.rb +66 -10
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 611d7fa815981b0a68443f7641f2f780afe46bca6fd8fed0f6204f35b0c2721b
|
|
4
|
+
data.tar.gz: c414565ed46cb55375c7ec599424dea459999c2162e458ee62be81a49dcf601b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8e1fa4bbc10f70ba9e17f01292228d538d639f3de6a4e1fc30207f436a1d72133edf502eef557405fce096ecb30c4c9125cd6919f3dc03cb5005e356fcb11f4
|
|
7
|
+
data.tar.gz: b3904a0fa64f33251e6db3cf6f454578a3dadccb75f8e3b3e885de834ecd6c9b0e0dd53aeaa62587fe9dde5979b991f0b54dc93e10b835ab9bfdad8cd31f4136
|
data/README.md
CHANGED
|
@@ -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
|
|
@@ -43,33 +55,70 @@ sin = Sashite::Sin.parse("c")
|
|
|
43
55
|
sin.abbr # => :C
|
|
44
56
|
sin.side # => :second
|
|
45
57
|
|
|
58
|
+
# Returns a cached instance — no allocation
|
|
59
|
+
Sashite::Sin.parse("C").equal?(Sashite::Sin.parse("C")) # => true
|
|
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
|
|
|
66
|
+
### Safe Parsing (String → Identifier | nil)
|
|
67
|
+
|
|
68
|
+
Parse without raising exceptions. Returns `nil` on invalid input.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
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
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Fetching by Components (Symbol, Symbol → Identifier)
|
|
83
|
+
|
|
84
|
+
Retrieve a cached identifier directly by abbreviation and side, bypassing string parsing entirely.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
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>
|
|
90
|
+
|
|
91
|
+
# Same cached instance as parse
|
|
92
|
+
Sashite::Sin.fetch(:C, :first).equal?(Sashite::Sin.parse("C")) # => true
|
|
93
|
+
|
|
94
|
+
# Invalid components raise ArgumentError
|
|
95
|
+
Sashite::Sin.fetch(:CC, :first) # => raises ArgumentError
|
|
96
|
+
Sashite::Sin.fetch(:C, :third) # => raises ArgumentError
|
|
97
|
+
```
|
|
98
|
+
|
|
51
99
|
### Formatting (Identifier → String)
|
|
52
100
|
|
|
53
101
|
Convert an `Identifier` back to a SIN string.
|
|
54
102
|
|
|
55
103
|
```ruby
|
|
56
|
-
|
|
57
|
-
sin = Sashite::Sin::Identifier.new(:C, :first)
|
|
104
|
+
sin = Sashite::Sin.parse("C")
|
|
58
105
|
sin.to_s # => "C"
|
|
59
106
|
|
|
60
|
-
sin = Sashite::Sin
|
|
107
|
+
sin = Sashite::Sin.parse("c")
|
|
61
108
|
sin.to_s # => "c"
|
|
62
109
|
```
|
|
63
110
|
|
|
64
111
|
### Validation
|
|
65
112
|
|
|
66
113
|
```ruby
|
|
67
|
-
# Boolean check
|
|
114
|
+
# Boolean check (never raises)
|
|
115
|
+
# Uses an exception-free code path internally for performance.
|
|
68
116
|
Sashite::Sin.valid?("C") # => true
|
|
69
117
|
Sashite::Sin.valid?("c") # => true
|
|
70
118
|
Sashite::Sin.valid?("") # => false
|
|
71
119
|
Sashite::Sin.valid?("CC") # => false
|
|
72
120
|
Sashite::Sin.valid?("1") # => false
|
|
121
|
+
Sashite::Sin.valid?(nil) # => false
|
|
73
122
|
```
|
|
74
123
|
|
|
75
124
|
### Queries
|
|
@@ -89,22 +138,55 @@ sin.same_side?(other) # => false
|
|
|
89
138
|
|
|
90
139
|
## API Reference
|
|
91
140
|
|
|
92
|
-
###
|
|
141
|
+
### Module Methods
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# Parses a SIN string into a cached Identifier.
|
|
145
|
+
# Returns a pre-instantiated, frozen instance.
|
|
146
|
+
# Raises ArgumentError if the string is not valid.
|
|
147
|
+
#
|
|
148
|
+
# @param string [String] SIN string
|
|
149
|
+
# @return [Identifier]
|
|
150
|
+
# @raise [ArgumentError] if invalid
|
|
151
|
+
def Sashite::Sin.parse(string)
|
|
152
|
+
|
|
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
|
+
|
|
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.
|
|
174
|
+
#
|
|
175
|
+
# @param string [String] SIN string
|
|
176
|
+
# @return [Boolean]
|
|
177
|
+
def Sashite::Sin.valid?(string)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Identifier
|
|
93
181
|
|
|
94
182
|
```ruby
|
|
95
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.
|
|
96
186
|
class Sashite::Sin::Identifier
|
|
97
|
-
# Creates an Identifier from abbreviation and side.
|
|
98
|
-
# Raises ArgumentError if attributes are invalid.
|
|
99
|
-
#
|
|
100
|
-
# @param abbr [Symbol] Style abbreviation (:A through :Z)
|
|
101
|
-
# @param side [Symbol] Player side (:first or :second)
|
|
102
|
-
# @return [Identifier]
|
|
103
|
-
def initialize(abbr, side)
|
|
104
|
-
|
|
105
187
|
# Returns the style abbreviation as an uppercase symbol.
|
|
106
188
|
#
|
|
107
|
-
# @return [Symbol]
|
|
189
|
+
# @return [Symbol] :A through :Z
|
|
108
190
|
def abbr
|
|
109
191
|
|
|
110
192
|
# Returns the player side.
|
|
@@ -119,35 +201,6 @@ class Sashite::Sin::Identifier
|
|
|
119
201
|
end
|
|
120
202
|
```
|
|
121
203
|
|
|
122
|
-
### Constants
|
|
123
|
-
|
|
124
|
-
```ruby
|
|
125
|
-
Sashite::Sin::Identifier::VALID_ABBRS # => [:A, :B, ..., :Z]
|
|
126
|
-
Sashite::Sin::Identifier::VALID_SIDES # => [:first, :second]
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Parsing
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
# Parses a SIN string into an Identifier.
|
|
133
|
-
# Raises ArgumentError if the string is not valid.
|
|
134
|
-
#
|
|
135
|
-
# @param string [String] SIN string
|
|
136
|
-
# @return [Identifier]
|
|
137
|
-
# @raise [ArgumentError] if invalid
|
|
138
|
-
def Sashite::Sin.parse(string)
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Validation
|
|
142
|
-
|
|
143
|
-
```ruby
|
|
144
|
-
# Reports whether string is a valid SIN identifier.
|
|
145
|
-
#
|
|
146
|
-
# @param string [String] SIN string
|
|
147
|
-
# @return [Boolean]
|
|
148
|
-
def Sashite::Sin.valid?(string)
|
|
149
|
-
```
|
|
150
|
-
|
|
151
204
|
### Queries
|
|
152
205
|
|
|
153
206
|
```ruby
|
|
@@ -160,9 +213,16 @@ def same_abbr?(other) # => Boolean
|
|
|
160
213
|
def same_side?(other) # => Boolean
|
|
161
214
|
```
|
|
162
215
|
|
|
216
|
+
### Constants
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
Sashite::Sin::Identifier::VALID_ABBRS # => [:A, :B, ..., :Z]
|
|
220
|
+
Sashite::Sin::Identifier::VALID_SIDES # => [:first, :second]
|
|
221
|
+
```
|
|
222
|
+
|
|
163
223
|
### Errors
|
|
164
224
|
|
|
165
|
-
All
|
|
225
|
+
All errors raise `ArgumentError` with descriptive messages:
|
|
166
226
|
|
|
167
227
|
| Message | Cause |
|
|
168
228
|
|---------|-------|
|
|
@@ -172,12 +232,28 @@ All parsing and validation errors raise `ArgumentError` with descriptive message
|
|
|
172
232
|
|
|
173
233
|
## Design Principles
|
|
174
234
|
|
|
175
|
-
- **
|
|
176
|
-
- **
|
|
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
|
|
177
238
|
- **Ruby idioms**: `valid?` predicate, `to_s` conversion, `ArgumentError` for invalid input
|
|
178
|
-
- **Immutable identifiers**:
|
|
239
|
+
- **Immutable identifiers**: All instances are frozen after creation
|
|
179
240
|
- **No dependencies**: Pure Ruby standard library only
|
|
180
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
|
+
|
|
181
257
|
## Related Specifications
|
|
182
258
|
|
|
183
259
|
- [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
|
|
@@ -4,13 +4,8 @@ module Sashite
|
|
|
4
4
|
module Sin
|
|
5
5
|
# Constants for the SIN (Style Identifier Notation) specification.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# @example Accessing valid abbreviations
|
|
10
|
-
# Constants::VALID_ABBRS # => [: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
|
|
@@ -24,15 +19,38 @@ module Sashite
|
|
|
24
19
|
# @return [Array<Symbol>] Array of valid side symbols
|
|
25
20
|
VALID_SIDES = %i[first second].freeze
|
|
26
21
|
|
|
27
|
-
#
|
|
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
|
-
# @
|
|
30
|
-
|
|
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
|
-
#
|
|
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 [
|
|
35
|
-
|
|
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
|
|
@@ -11,15 +11,16 @@ module Sashite
|
|
|
11
11
|
# - Abbr: the style abbreviation (A-Z as uppercase symbol)
|
|
12
12
|
# - Side: the player side (:first or :second)
|
|
13
13
|
#
|
|
14
|
-
#
|
|
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
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
|
21
|
-
#
|
|
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
|
|
@@ -42,15 +43,16 @@ module Sashite
|
|
|
42
43
|
# @return [Identifier] A new frozen Identifier instance
|
|
43
44
|
# @raise [Errors::Argument] If any attribute is invalid
|
|
44
45
|
#
|
|
45
|
-
# @
|
|
46
|
-
# Identifier.new(:C, :first)
|
|
47
|
-
# Identifier.new(:S, :second)
|
|
46
|
+
# @api private
|
|
48
47
|
def initialize(abbr, side)
|
|
49
48
|
validate_abbr!(abbr)
|
|
50
49
|
validate_side!(side)
|
|
51
50
|
|
|
52
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,18 +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
|
-
#
|
|
68
|
-
#
|
|
71
|
+
# Sashite::Sin.parse("C").to_s # => "C"
|
|
72
|
+
# Sashite::Sin.parse("c").to_s # => "c"
|
|
69
73
|
def to_s
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
case side
|
|
73
|
-
when :first then base.upcase
|
|
74
|
-
when :second then base.downcase
|
|
75
|
-
end
|
|
74
|
+
@string
|
|
76
75
|
end
|
|
77
76
|
|
|
78
77
|
# ========================================================================
|
|
@@ -84,7 +83,7 @@ module Sashite
|
|
|
84
83
|
# @return [Boolean] true if first player
|
|
85
84
|
#
|
|
86
85
|
# @example
|
|
87
|
-
#
|
|
86
|
+
# Sashite::Sin.parse("C").first_player? # => true
|
|
88
87
|
def first_player?
|
|
89
88
|
side.equal?(:first)
|
|
90
89
|
end
|
|
@@ -94,7 +93,7 @@ module Sashite
|
|
|
94
93
|
# @return [Boolean] true if second player
|
|
95
94
|
#
|
|
96
95
|
# @example
|
|
97
|
-
#
|
|
96
|
+
# Sashite::Sin.parse("c").second_player? # => true
|
|
98
97
|
def second_player?
|
|
99
98
|
side.equal?(:second)
|
|
100
99
|
end
|
|
@@ -109,8 +108,8 @@ module Sashite
|
|
|
109
108
|
# @return [Boolean] true if same abbreviation
|
|
110
109
|
#
|
|
111
110
|
# @example
|
|
112
|
-
# sin1 =
|
|
113
|
-
# sin2 =
|
|
111
|
+
# sin1 = Sashite::Sin.parse("C")
|
|
112
|
+
# sin2 = Sashite::Sin.parse("c")
|
|
114
113
|
# sin1.same_abbr?(sin2) # => true
|
|
115
114
|
def same_abbr?(other)
|
|
116
115
|
abbr.equal?(other.abbr)
|
|
@@ -122,8 +121,8 @@ module Sashite
|
|
|
122
121
|
# @return [Boolean] true if same side
|
|
123
122
|
#
|
|
124
123
|
# @example
|
|
125
|
-
# sin1 =
|
|
126
|
-
# sin2 =
|
|
124
|
+
# sin1 = Sashite::Sin.parse("C")
|
|
125
|
+
# sin2 = Sashite::Sin.parse("S")
|
|
127
126
|
# sin1.same_side?(sin2) # => true
|
|
128
127
|
def same_side?(other)
|
|
129
128
|
side.equal?(other.side)
|
|
@@ -135,36 +134,36 @@ module Sashite
|
|
|
135
134
|
|
|
136
135
|
# Checks equality with another Identifier.
|
|
137
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
|
+
#
|
|
138
141
|
# @param other [Object] The object to compare
|
|
139
142
|
# @return [Boolean] true if equal
|
|
140
143
|
#
|
|
141
144
|
# @example
|
|
142
|
-
#
|
|
143
|
-
# sin2 = Identifier.new(:C, :first)
|
|
144
|
-
# sin1 == sin2 # => true
|
|
145
|
+
# Sashite::Sin.parse("C") == Sashite::Sin.fetch(:C, :first) # => true
|
|
145
146
|
def ==(other)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
abbr.equal?(other.abbr) && side.equal?(other.side)
|
|
147
|
+
equal?(other) || (self.class === other && abbr.equal?(other.abbr) && side.equal?(other.side))
|
|
149
148
|
end
|
|
150
149
|
|
|
151
150
|
alias eql? ==
|
|
152
151
|
|
|
153
|
-
# Returns a hash code for the Identifier.
|
|
152
|
+
# Returns a pre-computed hash code for the Identifier.
|
|
154
153
|
#
|
|
155
154
|
# @return [Integer] Hash code
|
|
156
155
|
def hash
|
|
157
|
-
|
|
156
|
+
@hash
|
|
158
157
|
end
|
|
159
158
|
|
|
160
|
-
# Returns
|
|
159
|
+
# Returns a pre-computed inspect string for the Identifier.
|
|
161
160
|
#
|
|
162
161
|
# @return [String] Inspect representation
|
|
163
162
|
#
|
|
164
163
|
# @example
|
|
165
|
-
#
|
|
164
|
+
# Sashite::Sin.parse("C").inspect # => "#<Sashite::Sin::Identifier C>"
|
|
166
165
|
def inspect
|
|
167
|
-
|
|
166
|
+
@inspect
|
|
168
167
|
end
|
|
169
168
|
|
|
170
169
|
private
|
|
@@ -184,6 +183,38 @@ module Sashite
|
|
|
184
183
|
|
|
185
184
|
raise Errors::Argument, Errors::Argument::Messages::INVALID_SIDE
|
|
186
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
|
|
187
218
|
end
|
|
188
219
|
end
|
|
189
220
|
end
|
data/lib/sashite/sin/parser.rb
CHANGED
|
@@ -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
|
-
#
|
|
11
|
-
#
|
|
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") # =>
|
|
15
|
-
# Parser.parse("c") # =>
|
|
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
|
|
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 [
|
|
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") # =>
|
|
31
|
-
# Parser.parse("s") # =>
|
|
46
|
+
# Parser.parse("C") # => #<Sashite::Sin::Identifier C>
|
|
47
|
+
# Parser.parse("s") # => #<Sashite::Sin::Identifier s>
|
|
32
48
|
def self.parse(input)
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
true
|
|
56
|
-
rescue Errors::Argument
|
|
57
|
-
false
|
|
84
|
+
!safe_parse(input).nil?
|
|
58
85
|
end
|
|
59
86
|
|
|
60
|
-
#
|
|
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
|
|
65
|
-
# @
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return if
|
|
69
|
-
|
|
70
|
-
|
|
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 abbr and side from a validated byte.
|
|
107
|
-
#
|
|
108
|
-
# @param byte [Integer] A validated ASCII letter byte
|
|
109
|
-
# @return [Hash] Hash with :abbr and :side keys
|
|
110
|
-
private_class_method def self.extract_components(byte)
|
|
111
|
-
if uppercase_letter?(byte)
|
|
112
|
-
{ abbr: byte.chr.to_sym, side: :first }
|
|
113
|
-
else
|
|
114
|
-
{ abbr: 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,15 +15,26 @@ 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
24
|
# sin.abbr # => :C
|
|
21
25
|
# sin.side # => :first
|
|
22
26
|
# sin.to_s # => "C"
|
|
23
27
|
#
|
|
24
|
-
# @example
|
|
25
|
-
#
|
|
26
|
-
#
|
|
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
|
|
27
38
|
#
|
|
28
39
|
# @example Validation
|
|
29
40
|
# Sashite::Sin.valid?("C") # => true
|
|
@@ -31,10 +42,13 @@ module Sashite
|
|
|
31
42
|
#
|
|
32
43
|
# @see https://sashite.dev/specs/sin/1.0.0/
|
|
33
44
|
module Sin
|
|
34
|
-
# Parses a SIN string into
|
|
45
|
+
# Parses a SIN string into a cached Identifier.
|
|
46
|
+
#
|
|
47
|
+
# Returns a pre-instantiated, frozen instance.
|
|
48
|
+
# Raises ArgumentError if the string is not valid.
|
|
35
49
|
#
|
|
36
|
-
# @param input [String]
|
|
37
|
-
# @return [Identifier]
|
|
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)
|
|
@@ -47,14 +61,55 @@ module Sashite
|
|
|
47
61
|
# sin.abbr # => :C
|
|
48
62
|
# sin.side # => :second
|
|
49
63
|
def self.parse(input)
|
|
50
|
-
|
|
64
|
+
Parser.parse(input)
|
|
65
|
+
end
|
|
51
66
|
|
|
52
|
-
|
|
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
|
|
82
|
+
|
|
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
|
|
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]
|
|
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
|
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.
|
|
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.
|
|
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
|