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 +4 -4
- data/LICENSE +1 -1
- data/README.md +130 -95
- data/lib/sashite/sin/constants.rb +34 -16
- data/lib/sashite/sin/errors/argument/messages.rb +2 -2
- data/lib/sashite/sin/identifier.rb +83 -113
- data/lib/sashite/sin/parser.rb +56 -93
- data/lib/sashite/sin.rb +73 -17
- data/lib/sashite-sin.rb +0 -11
- 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/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
|
[](https://github.com/sashite/sin.rb/tags)
|
|
4
4
|
[](https://rubydoc.info/github/sashite/sin.rb/main)
|
|
5
|
-
[](https://github.com/sashite/sin.rb/actions)
|
|
6
6
|
[](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.
|
|
39
|
-
sin.side
|
|
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.
|
|
44
|
-
sin.side
|
|
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
|
-
###
|
|
66
|
+
### Safe Parsing (String → Identifier | nil)
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
Parse without raising exceptions. Returns `nil` on invalid input.
|
|
54
69
|
|
|
55
70
|
```ruby
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
###
|
|
82
|
+
### Fetching by Components (Symbol, Symbol → Identifier)
|
|
65
83
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
81
|
-
|
|
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
|
-
#
|
|
85
|
-
|
|
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
|
-
###
|
|
99
|
+
### Formatting (Identifier → String)
|
|
89
100
|
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
sin.
|
|
107
|
+
sin = Sashite::Sin.parse("c")
|
|
108
|
+
sin.to_s # => "c"
|
|
109
|
+
```
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
115
|
-
sin.same_side?(other)
|
|
135
|
+
sin.same_abbr?(other) # => true
|
|
136
|
+
sin.same_side?(other) # => false
|
|
116
137
|
```
|
|
117
138
|
|
|
118
139
|
## API Reference
|
|
119
140
|
|
|
120
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
180
|
-
|
|
181
|
-
All transformations return new `Sashite::Sin::Identifier` objects:
|
|
180
|
+
### Identifier
|
|
182
181
|
|
|
183
182
|
```ruby
|
|
184
|
-
#
|
|
185
|
-
|
|
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
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
201
|
-
def same_side?(other)
|
|
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
|
|
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
|
-
- **
|
|
217
|
-
- **
|
|
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
|
|
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
|
-
#
|
|
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
|
|
12
|
+
# Valid abbreviation symbols (A-Z as uppercase symbols).
|
|
18
13
|
#
|
|
19
|
-
# @return [Array<Symbol>] Array of 26 valid
|
|
20
|
-
|
|
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
|
-
#
|
|
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
|
|
@@ -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
|
|
32
|
+
# Error message for invalid abbreviation value.
|
|
33
33
|
#
|
|
34
34
|
# @return [String] Error message
|
|
35
|
-
|
|
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
|
-
# -
|
|
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
|
|
26
|
-
# Valid
|
|
27
|
-
|
|
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]
|
|
33
|
-
attr_reader :
|
|
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
|
|
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
|
-
# @
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
@
|
|
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
|
-
#
|
|
68
|
-
#
|
|
71
|
+
# Sashite::Sin.parse("C").to_s # => "C"
|
|
72
|
+
# Sashite::Sin.parse("c").to_s # => "c"
|
|
69
73
|
def to_s
|
|
70
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
108
|
+
# @return [Boolean] true if same abbreviation
|
|
171
109
|
#
|
|
172
110
|
# @example
|
|
173
|
-
# sin1 =
|
|
174
|
-
# sin2 =
|
|
175
|
-
# sin1.
|
|
176
|
-
def
|
|
177
|
-
|
|
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 =
|
|
187
|
-
# sin2 =
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
@hash
|
|
219
157
|
end
|
|
220
158
|
|
|
221
|
-
# Returns
|
|
159
|
+
# Returns a pre-computed inspect string for the Identifier.
|
|
222
160
|
#
|
|
223
161
|
# @return [String] Inspect representation
|
|
224
162
|
#
|
|
225
163
|
# @example
|
|
226
|
-
#
|
|
164
|
+
# Sashite::Sin.parse("C").inspect # => "#<Sashite::Sin::Identifier C>"
|
|
227
165
|
def inspect
|
|
228
|
-
|
|
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
|
|
238
|
-
return if ::Symbol ===
|
|
175
|
+
def validate_abbr!(abbr)
|
|
176
|
+
return if ::Symbol === abbr && Constants::VALID_ABBRS.include?(abbr)
|
|
239
177
|
|
|
240
|
-
raise Errors::Argument, Errors::Argument::Messages::
|
|
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
|
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 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.
|
|
21
|
-
# sin.side
|
|
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
|
|
45
|
+
# Parses a SIN string into a cached Identifier.
|
|
35
46
|
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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.
|
|
43
|
-
# sin.side
|
|
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.
|
|
48
|
-
# sin.side
|
|
61
|
+
# sin.abbr # => :C
|
|
62
|
+
# sin.side # => :second
|
|
49
63
|
def self.parse(input)
|
|
50
|
-
|
|
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
|
-
|
|
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
|
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.
|
|
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
|