sashite-cell 3.0.0 → 4.0.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 +29 -13
- data/lib/sashite/cell/constants.rb +19 -0
- data/lib/sashite/cell/coordinate.rb +28 -15
- data/lib/sashite/cell/errors/argument/messages.rb +22 -0
- data/lib/sashite/cell/errors/argument.rb +30 -0
- data/lib/sashite/cell/errors.rb +3 -0
- data/lib/sashite/cell/formatter.rb +102 -0
- data/lib/sashite/cell/parser.rb +187 -73
- data/lib/sashite/cell.rb +9 -7
- metadata +6 -2
- data/lib/sashite/cell/dumper.rb +0 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac0688e4dc618a3549c4b3d62db1bad731255cea5b239a87e85ac7017ce47869
|
|
4
|
+
data.tar.gz: 44ed86405deacc70b71982e07ffd2337ce9600682aab31bf524f5e2d4a9ff40d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee5405dba3da3d7f8825cfd9af6220a42f34b3ca2b90a3f07b1fca3151f30d0e59e8c154468c1c57430d4e9925e531e3db81d521cc98c0756c69ecabae088fbc
|
|
7
|
+
data.tar.gz: 640048dd47362c161dbb1e3f3e6b7ea560fd4f80b915f5de37e5854f870b17ac7d511e6ddc6c96c274c60ca53b4d598abec23ce5ad59d048835e9f6277ace309
|
data/README.md
CHANGED
|
@@ -52,8 +52,8 @@ coord.dimensions # => 2
|
|
|
52
52
|
coord = Sashite::Cell.parse("a1A")
|
|
53
53
|
coord.indices # => [0, 0, 0]
|
|
54
54
|
|
|
55
|
-
# Invalid input raises
|
|
56
|
-
Sashite::Cell.parse("a0") # => raises
|
|
55
|
+
# Invalid input raises Sashite::Cell::Errors::Argument
|
|
56
|
+
Sashite::Cell.parse("a0") # => raises Sashite::Cell::Errors::Argument
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
### Formatting (Coordinate → String)
|
|
@@ -76,7 +76,7 @@ Sashite::Cell.format(2, 2, 2) # => "c3C"
|
|
|
76
76
|
Sashite::Cell.valid?("e4") # => true
|
|
77
77
|
|
|
78
78
|
# Detailed error
|
|
79
|
-
Sashite::Cell.validate("a0") # => raises
|
|
79
|
+
Sashite::Cell.validate("a0") # => raises Sashite::Cell::Errors::Argument, "leading zero"
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
### Accessing Coordinate Data
|
|
@@ -103,7 +103,7 @@ coord.indices[1] # => 3
|
|
|
103
103
|
# Coordinate represents a parsed CELL coordinate with up to 3 dimensions.
|
|
104
104
|
class Sashite::Cell::Coordinate
|
|
105
105
|
# Creates a Coordinate from 1 to 3 indices.
|
|
106
|
-
# Raises
|
|
106
|
+
# Raises Sashite::Cell::Errors::Argument if no indices provided or more than 3.
|
|
107
107
|
#
|
|
108
108
|
# @param indices [Array<Integer>] 0-indexed coordinate values (0-255)
|
|
109
109
|
# @return [Coordinate]
|
|
@@ -129,20 +129,20 @@ end
|
|
|
129
129
|
### Constants
|
|
130
130
|
|
|
131
131
|
```ruby
|
|
132
|
-
Sashite::Cell::
|
|
133
|
-
Sashite::Cell::
|
|
134
|
-
Sashite::Cell::
|
|
132
|
+
Sashite::Cell::Constants::MAX_DIMENSIONS # => 3
|
|
133
|
+
Sashite::Cell::Constants::MAX_INDEX_VALUE # => 255
|
|
134
|
+
Sashite::Cell::Constants::MAX_STRING_LENGTH # => 7
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
### Parsing
|
|
138
138
|
|
|
139
139
|
```ruby
|
|
140
140
|
# Parses a CELL string into a Coordinate.
|
|
141
|
-
# Raises
|
|
141
|
+
# Raises Sashite::Cell::Errors::Argument if the string is not valid.
|
|
142
142
|
#
|
|
143
143
|
# @param string [String] CELL coordinate string
|
|
144
144
|
# @return [Coordinate]
|
|
145
|
-
# @raise [
|
|
145
|
+
# @raise [Sashite::Cell::Errors::Argument] if invalid
|
|
146
146
|
def Sashite::Cell.parse(string)
|
|
147
147
|
```
|
|
148
148
|
|
|
@@ -161,11 +161,11 @@ def Sashite::Cell.format(*indices)
|
|
|
161
161
|
|
|
162
162
|
```ruby
|
|
163
163
|
# Validates a CELL string.
|
|
164
|
-
# Raises
|
|
164
|
+
# Raises Sashite::Cell::Errors::Argument with descriptive message if invalid.
|
|
165
165
|
#
|
|
166
166
|
# @param string [String] CELL coordinate string
|
|
167
167
|
# @return [nil]
|
|
168
|
-
# @raise [
|
|
168
|
+
# @raise [Sashite::Cell::Errors::Argument] if invalid
|
|
169
169
|
def Sashite::Cell.validate(string)
|
|
170
170
|
|
|
171
171
|
# Reports whether string is a valid CELL coordinate.
|
|
@@ -177,7 +177,7 @@ def Sashite::Cell.valid?(string)
|
|
|
177
177
|
|
|
178
178
|
### Errors
|
|
179
179
|
|
|
180
|
-
All parsing and validation errors raise `ArgumentError` with descriptive messages:
|
|
180
|
+
All parsing and validation errors raise `Sashite::Cell::Errors::Argument` (a subclass of `ArgumentError`) with descriptive messages:
|
|
181
181
|
|
|
182
182
|
| Message | Cause |
|
|
183
183
|
|---------|-------|
|
|
@@ -189,11 +189,27 @@ All parsing and validation errors raise `ArgumentError` with descriptive message
|
|
|
189
189
|
| `"exceeds 3 dimensions"` | More than 3 dimensions |
|
|
190
190
|
| `"index exceeds 255"` | Decoded value out of range |
|
|
191
191
|
|
|
192
|
+
```ruby
|
|
193
|
+
begin
|
|
194
|
+
Sashite::Cell.parse("a0")
|
|
195
|
+
rescue Sashite::Cell::Errors::Argument => e
|
|
196
|
+
puts e.message # => "leading zero"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Also catchable as ArgumentError for compatibility
|
|
200
|
+
begin
|
|
201
|
+
Sashite::Cell.parse("a0")
|
|
202
|
+
rescue ArgumentError => e
|
|
203
|
+
puts e.message # => "leading zero"
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
192
207
|
## Design Principles
|
|
193
208
|
|
|
194
209
|
- **Bounded values**: Index validation prevents overflow
|
|
195
210
|
- **Object-oriented**: `Coordinate` class enables methods and encapsulation
|
|
196
|
-
- **Ruby idioms**: `valid?` predicate, `to_s` conversion
|
|
211
|
+
- **Ruby idioms**: `valid?` predicate, `to_s` conversion
|
|
212
|
+
- **Custom error class**: `Errors::Argument` inherits from `ArgumentError` for precise error handling
|
|
197
213
|
- **Immutable coordinates**: Frozen indices array prevents mutation
|
|
198
214
|
- **No dependencies**: Pure Ruby standard library only
|
|
199
215
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Cell
|
|
5
|
+
module Constants
|
|
6
|
+
# Maximum number of dimensions supported by a CELL coordinate.
|
|
7
|
+
# Sufficient for 1D, 2D, and 3D game boards.
|
|
8
|
+
MAX_DIMENSIONS = 3
|
|
9
|
+
|
|
10
|
+
# Maximum value for a single coordinate index.
|
|
11
|
+
# Fits in an 8-bit unsigned integer (0-255).
|
|
12
|
+
MAX_INDEX_VALUE = 255
|
|
13
|
+
|
|
14
|
+
# Maximum length of a CELL string representation.
|
|
15
|
+
# Corresponds to "iv256IV" (worst case for all dimensions at 255).
|
|
16
|
+
MAX_STRING_LENGTH = 7
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "formatter"
|
|
6
|
+
|
|
3
7
|
module Sashite
|
|
4
8
|
module Cell
|
|
5
9
|
# Represents a parsed CELL coordinate with up to 3 dimensions.
|
|
@@ -17,35 +21,34 @@ module Sashite
|
|
|
17
21
|
# coord = Sashite::Cell::Coordinate.new(0, 0, 0)
|
|
18
22
|
# coord.to_s # => "a1A"
|
|
19
23
|
class Coordinate
|
|
20
|
-
# Maximum number of dimensions supported.
|
|
21
|
-
MAX_DIMENSIONS = 3
|
|
22
|
-
|
|
23
|
-
# Maximum value for any single index (0-255).
|
|
24
|
-
MAX_INDEX_VALUE = 255
|
|
25
|
-
|
|
26
|
-
# Maximum length of a CELL coordinate string.
|
|
27
|
-
MAX_STRING_LENGTH = 7
|
|
28
|
-
|
|
29
24
|
# Returns the coordinate indices as a frozen array.
|
|
30
25
|
#
|
|
31
26
|
# @return [Array<Integer>] 0-indexed coordinate values
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# Sashite::Cell::Coordinate.new(4, 3).indices # => [4, 3]
|
|
32
30
|
attr_reader :indices
|
|
33
31
|
|
|
34
32
|
# Creates a Coordinate from 1 to 3 indices.
|
|
35
33
|
#
|
|
36
34
|
# @param indices [Array<Integer>] 0-indexed coordinate values (0-255 each)
|
|
37
|
-
# @raise [
|
|
35
|
+
# @raise [Sashite::Cell::Errors::Argument] if no indices provided, more than 3, or values out of range
|
|
38
36
|
#
|
|
39
37
|
# @example
|
|
40
38
|
# Sashite::Cell::Coordinate.new(4, 3) # 2D coordinate
|
|
41
39
|
# Sashite::Cell::Coordinate.new(0, 0, 0) # 3D coordinate
|
|
42
40
|
def initialize(*indices)
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if indices.empty?
|
|
42
|
+
raise Errors::Argument, Errors::Argument::Messages::NO_INDICES
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if indices.size > Constants::MAX_DIMENSIONS
|
|
46
|
+
raise Errors::Argument, Errors::Argument::Messages::TOO_MANY_DIMENSIONS
|
|
47
|
+
end
|
|
45
48
|
|
|
46
49
|
indices.each do |index|
|
|
47
|
-
unless index.is_a?(::Integer) && index >= 0 && index <= MAX_INDEX_VALUE
|
|
48
|
-
raise ::
|
|
50
|
+
unless index.is_a?(::Integer) && index >= 0 && index <= Constants::MAX_INDEX_VALUE
|
|
51
|
+
raise Errors::Argument, Errors::Argument::Messages::INDEX_OUT_OF_RANGE
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
|
|
@@ -69,13 +72,16 @@ module Sashite
|
|
|
69
72
|
# @example
|
|
70
73
|
# Sashite::Cell::Coordinate.new(4, 3).to_s # => "e4"
|
|
71
74
|
def to_s
|
|
72
|
-
|
|
75
|
+
Formatter.indices_to_string(@indices)
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
# Checks equality with another Coordinate.
|
|
76
79
|
#
|
|
77
80
|
# @param other [Object] object to compare
|
|
78
81
|
# @return [Boolean] true if equal, false otherwise
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# Sashite::Cell::Coordinate.new(4, 3) == Sashite::Cell::Coordinate.new(4, 3) # => true
|
|
79
85
|
def ==(other)
|
|
80
86
|
other.is_a?(Coordinate) && @indices == other.indices
|
|
81
87
|
end
|
|
@@ -85,6 +91,10 @@ module Sashite
|
|
|
85
91
|
# Returns hash code for use in Hash keys.
|
|
86
92
|
#
|
|
87
93
|
# @return [Integer] hash code
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# coord = Sashite::Cell::Coordinate.new(4, 3)
|
|
97
|
+
# hash = { coord => "value" }
|
|
88
98
|
def hash
|
|
89
99
|
@indices.hash
|
|
90
100
|
end
|
|
@@ -92,6 +102,9 @@ module Sashite
|
|
|
92
102
|
# Returns a human-readable representation.
|
|
93
103
|
#
|
|
94
104
|
# @return [String] inspection string
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# Sashite::Cell::Coordinate.new(4, 3).inspect # => "#<Sashite::Cell::Coordinate e4>"
|
|
95
108
|
def inspect
|
|
96
109
|
"#<#{self.class} #{self}>"
|
|
97
110
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Cell
|
|
5
|
+
module Errors
|
|
6
|
+
class Argument < ::ArgumentError
|
|
7
|
+
# Error messages for validation failures.
|
|
8
|
+
# Kept as constants to ensure consistency across the library.
|
|
9
|
+
module Messages
|
|
10
|
+
EMPTY_INPUT = "empty input"
|
|
11
|
+
INPUT_TOO_LONG = "input exceeds 7 characters"
|
|
12
|
+
INVALID_START = "must start with lowercase letter"
|
|
13
|
+
UNEXPECTED_CHARACTER = "unexpected character"
|
|
14
|
+
LEADING_ZERO = "leading zero"
|
|
15
|
+
TOO_MANY_DIMENSIONS = "exceeds 3 dimensions"
|
|
16
|
+
INDEX_OUT_OF_RANGE = "index exceeds 255"
|
|
17
|
+
NO_INDICES = "at least one index required"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "argument/messages"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Cell
|
|
7
|
+
module Errors
|
|
8
|
+
# Custom error class for CELL parsing and validation failures.
|
|
9
|
+
#
|
|
10
|
+
# Inherits from ArgumentError to maintain semantic meaning
|
|
11
|
+
# while allowing specific rescue of CELL-related errors.
|
|
12
|
+
#
|
|
13
|
+
# @example Rescuing specific CELL errors
|
|
14
|
+
# begin
|
|
15
|
+
# Sashite::Cell.parse("invalid")
|
|
16
|
+
# rescue Sashite::Cell::Errors::Argument => e
|
|
17
|
+
# puts "CELL error: #{e.message}"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Rescuing as ArgumentError
|
|
21
|
+
# begin
|
|
22
|
+
# Sashite::Cell.parse("invalid")
|
|
23
|
+
# rescue ArgumentError => e
|
|
24
|
+
# puts "Argument error: #{e.message}"
|
|
25
|
+
# end
|
|
26
|
+
class Argument < ::ArgumentError
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Cell
|
|
5
|
+
# Formats index arrays into CELL coordinate strings.
|
|
6
|
+
#
|
|
7
|
+
# This module handles the conversion from numeric indices to their
|
|
8
|
+
# CELL string representation following the cyclic pattern:
|
|
9
|
+
# - Dimension 1, 4, 7...: lowercase letters (a-z, aa-iv)
|
|
10
|
+
# - Dimension 2, 5, 8...: positive integers (1-256)
|
|
11
|
+
# - Dimension 3, 6, 9...: uppercase letters (A-Z, AA-IV)
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# Formatter.indices_to_string([4, 3]) # => "e4"
|
|
15
|
+
# Formatter.indices_to_string([0, 0, 0]) # => "a1A"
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
module Formatter
|
|
19
|
+
# Formats an indices array to a CELL string.
|
|
20
|
+
#
|
|
21
|
+
# @param indices [Array<Integer>] 0-indexed coordinate values
|
|
22
|
+
# @return [String] CELL coordinate string
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# Formatter.indices_to_string([4, 3]) # => "e4"
|
|
26
|
+
# Formatter.indices_to_string([255, 255, 255]) # => "iv256IV"
|
|
27
|
+
def self.indices_to_string(indices)
|
|
28
|
+
result = +""
|
|
29
|
+
|
|
30
|
+
indices.each_with_index do |index, i|
|
|
31
|
+
dimension_type = i % 3
|
|
32
|
+
|
|
33
|
+
result << case dimension_type
|
|
34
|
+
when 0 then encode_to_lower(index)
|
|
35
|
+
when 1 then encode_to_number(index)
|
|
36
|
+
when 2 then encode_to_upper(index)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
result.freeze
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Encodes an index (0-255) as lowercase letters (a-z, aa-iv).
|
|
44
|
+
#
|
|
45
|
+
# @param index [Integer] 0-indexed value (0-255)
|
|
46
|
+
# @return [String] lowercase letter sequence
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# encode_to_lower(0) # => "a"
|
|
50
|
+
# encode_to_lower(25) # => "z"
|
|
51
|
+
# encode_to_lower(26) # => "aa"
|
|
52
|
+
# encode_to_lower(255) # => "iv"
|
|
53
|
+
private_class_method def self.encode_to_lower(index)
|
|
54
|
+
encode_to_letters(index, base: "a")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Encodes an index (0-255) as uppercase letters (A-Z, AA-IV).
|
|
58
|
+
#
|
|
59
|
+
# @param index [Integer] 0-indexed value (0-255)
|
|
60
|
+
# @return [String] uppercase letter sequence
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# encode_to_upper(0) # => "A"
|
|
64
|
+
# encode_to_upper(25) # => "Z"
|
|
65
|
+
# encode_to_upper(26) # => "AA"
|
|
66
|
+
# encode_to_upper(255) # => "IV"
|
|
67
|
+
private_class_method def self.encode_to_upper(index)
|
|
68
|
+
encode_to_letters(index, base: "A")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Encodes an index to a letter sequence.
|
|
72
|
+
#
|
|
73
|
+
# @param index [Integer] 0-indexed value (0-255)
|
|
74
|
+
# @param base [String] base character ("a" or "A")
|
|
75
|
+
# @return [String] letter sequence
|
|
76
|
+
private_class_method def self.encode_to_letters(index, base:)
|
|
77
|
+
base_ord = base.ord
|
|
78
|
+
|
|
79
|
+
if index < 26
|
|
80
|
+
(base_ord + index).chr
|
|
81
|
+
else
|
|
82
|
+
adjusted = index - 26
|
|
83
|
+
first = adjusted / 26
|
|
84
|
+
second = adjusted % 26
|
|
85
|
+
(base_ord + first).chr + (base_ord + second).chr
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Encodes an index (0-255) as a 1-based positive integer string.
|
|
90
|
+
#
|
|
91
|
+
# @param index [Integer] 0-indexed value (0-255)
|
|
92
|
+
# @return [String] number string (1-indexed)
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# encode_to_number(0) # => "1"
|
|
96
|
+
# encode_to_number(255) # => "256"
|
|
97
|
+
private_class_method def self.encode_to_number(index)
|
|
98
|
+
(index + 1).to_s
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/sashite/cell/parser.rb
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
3
6
|
module Sashite
|
|
4
7
|
module Cell
|
|
5
8
|
# Parses CELL coordinate strings into index arrays.
|
|
6
9
|
#
|
|
10
|
+
# This module handles the conversion from CELL string representation
|
|
11
|
+
# to numeric indices, following the cyclic pattern:
|
|
12
|
+
# - Dimension 1, 4, 7...: lowercase letters (a-z, aa-iv)
|
|
13
|
+
# - Dimension 2, 5, 8...: positive integers (1-256)
|
|
14
|
+
# - Dimension 3, 6, 9...: uppercase letters (A-Z, AA-IV)
|
|
15
|
+
#
|
|
16
|
+
# Security considerations:
|
|
17
|
+
# - Character-by-character parsing (no regex, no ReDoS risk)
|
|
18
|
+
# - Fail-fast on invalid input
|
|
19
|
+
# - Bounded iteration (max 7 characters)
|
|
20
|
+
# - Explicit ASCII validation
|
|
21
|
+
#
|
|
7
22
|
# @example
|
|
8
23
|
# Parser.parse_to_indices("e4") # => [4, 3]
|
|
9
24
|
# Parser.parse_to_indices("a1A") # => [0, 0, 0]
|
|
@@ -14,106 +29,205 @@ module Sashite
|
|
|
14
29
|
#
|
|
15
30
|
# @param string [String] CELL coordinate string
|
|
16
31
|
# @return [Array<Integer>] 0-indexed coordinate values
|
|
17
|
-
# @raise [
|
|
32
|
+
# @raise [Sashite::Cell::Errors::Argument] if parsing fails
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# Parser.parse_to_indices("e4") # => [4, 3]
|
|
36
|
+
# Parser.parse_to_indices("iv256IV") # => [255, 255, 255]
|
|
18
37
|
def self.parse_to_indices(string)
|
|
19
|
-
raise ::
|
|
20
|
-
if string.length > Coordinate::MAX_STRING_LENGTH
|
|
21
|
-
raise ::ArgumentError, "input exceeds #{Coordinate::MAX_STRING_LENGTH} characters"
|
|
22
|
-
end
|
|
23
|
-
raise ::ArgumentError, "must start with lowercase letter" unless string[0].match?(/[a-z]/)
|
|
38
|
+
raise Errors::Argument, Errors::Argument::Messages::EMPTY_INPUT if string.empty?
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
if string.length > Constants::MAX_STRING_LENGTH
|
|
41
|
+
raise Errors::Argument, Errors::Argument::Messages::INPUT_TOO_LONG
|
|
42
|
+
end
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
remaining = remaining[consumed..]
|
|
33
|
-
dimension += 1
|
|
44
|
+
first_byte = string.getbyte(0)
|
|
45
|
+
unless lowercase?(first_byte)
|
|
46
|
+
raise Errors::Argument, Errors::Argument::Messages::INVALID_START
|
|
34
47
|
end
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
indices = []
|
|
50
|
+
pos = 0
|
|
51
|
+
dimension_type = 0 # 0: lowercase, 1: integer, 2: uppercase
|
|
52
|
+
|
|
53
|
+
while pos < string.length
|
|
54
|
+
if indices.size >= Constants::MAX_DIMENSIONS
|
|
55
|
+
raise Errors::Argument, Errors::Argument::Messages::TOO_MANY_DIMENSIONS
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
case dimension_type
|
|
59
|
+
when 0
|
|
60
|
+
value, consumed = parse_lowercase(string, pos)
|
|
61
|
+
indices << value
|
|
62
|
+
pos += consumed
|
|
63
|
+
dimension_type = 1
|
|
64
|
+
when 1
|
|
65
|
+
value, consumed = parse_integer(string, pos)
|
|
66
|
+
indices << value
|
|
67
|
+
pos += consumed
|
|
68
|
+
dimension_type = 2
|
|
69
|
+
when 2
|
|
70
|
+
value, consumed = parse_uppercase(string, pos)
|
|
71
|
+
indices << value
|
|
72
|
+
pos += consumed
|
|
73
|
+
dimension_type = 0
|
|
74
|
+
end
|
|
38
75
|
end
|
|
39
76
|
|
|
40
77
|
indices
|
|
41
78
|
end
|
|
42
79
|
|
|
43
|
-
#
|
|
80
|
+
# Checks if a byte is a lowercase ASCII letter (a-z).
|
|
44
81
|
#
|
|
45
|
-
# @param
|
|
46
|
-
# @
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
# @param byte [Integer, nil] byte value
|
|
83
|
+
# @return [Boolean] true if lowercase letter
|
|
84
|
+
private_class_method def self.lowercase?(byte)
|
|
85
|
+
byte && byte >= 97 && byte <= 122 # 'a' = 97, 'z' = 122
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Checks if a byte is an uppercase ASCII letter (A-Z).
|
|
89
|
+
#
|
|
90
|
+
# @param byte [Integer, nil] byte value
|
|
91
|
+
# @return [Boolean] true if uppercase letter
|
|
92
|
+
private_class_method def self.uppercase?(byte)
|
|
93
|
+
byte && byte >= 65 && byte <= 90 # 'A' = 65, 'Z' = 90
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Checks if a byte is an ASCII digit (0-9).
|
|
97
|
+
#
|
|
98
|
+
# @param byte [Integer, nil] byte value
|
|
99
|
+
# @return [Boolean] true if digit
|
|
100
|
+
private_class_method def self.digit?(byte)
|
|
101
|
+
byte && byte >= 48 && byte <= 57 # '0' = 48, '9' = 57
|
|
55
102
|
end
|
|
56
103
|
|
|
57
|
-
# Parses lowercase letters
|
|
104
|
+
# Parses lowercase letters starting at position.
|
|
58
105
|
#
|
|
59
|
-
# @param
|
|
60
|
-
# @
|
|
61
|
-
# @
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
106
|
+
# @param string [String] input string
|
|
107
|
+
# @param pos [Integer] starting position
|
|
108
|
+
# @return [Array(Integer, Integer)] decoded value and characters consumed
|
|
109
|
+
# @raise [Sashite::Cell::Errors::Argument] if parsing fails
|
|
110
|
+
private_class_method def self.parse_lowercase(string, pos)
|
|
111
|
+
byte = string.getbyte(pos)
|
|
112
|
+
unless lowercase?(byte)
|
|
113
|
+
raise Errors::Argument, Errors::Argument::Messages::UNEXPECTED_CHARACTER
|
|
114
|
+
end
|
|
65
115
|
|
|
66
|
-
|
|
67
|
-
|
|
116
|
+
chars = [byte]
|
|
117
|
+
pos += 1
|
|
68
118
|
|
|
69
|
-
|
|
119
|
+
while pos < string.length && lowercase?(string.getbyte(pos))
|
|
120
|
+
chars << string.getbyte(pos)
|
|
121
|
+
pos += 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
value = decode_lowercase(chars)
|
|
125
|
+
if value > Constants::MAX_INDEX_VALUE
|
|
126
|
+
raise Errors::Argument, Errors::Argument::Messages::INDEX_OUT_OF_RANGE
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
[value, chars.size]
|
|
70
130
|
end
|
|
71
131
|
|
|
72
|
-
# Parses positive integer
|
|
132
|
+
# Parses a positive integer starting at position.
|
|
73
133
|
#
|
|
74
|
-
# @param
|
|
75
|
-
# @
|
|
76
|
-
# @
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
134
|
+
# @param string [String] input string
|
|
135
|
+
# @param pos [Integer] starting position
|
|
136
|
+
# @return [Array(Integer, Integer)] decoded value and characters consumed
|
|
137
|
+
# @raise [Sashite::Cell::Errors::Argument] if parsing fails
|
|
138
|
+
private_class_method def self.parse_integer(string, pos)
|
|
139
|
+
byte = string.getbyte(pos)
|
|
140
|
+
unless digit?(byte)
|
|
141
|
+
raise Errors::Argument, Errors::Argument::Messages::UNEXPECTED_CHARACTER
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check for leading zero
|
|
145
|
+
if byte == 48 # '0'
|
|
146
|
+
raise Errors::Argument, Errors::Argument::Messages::LEADING_ZERO
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
chars = [byte]
|
|
150
|
+
pos += 1
|
|
151
|
+
|
|
152
|
+
while pos < string.length && digit?(string.getbyte(pos))
|
|
153
|
+
chars << string.getbyte(pos)
|
|
154
|
+
pos += 1
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
value = decode_integer(chars)
|
|
158
|
+
if value < 0 || value > Constants::MAX_INDEX_VALUE
|
|
159
|
+
raise Errors::Argument, Errors::Argument::Messages::INDEX_OUT_OF_RANGE
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
[value, chars.size]
|
|
86
163
|
end
|
|
87
164
|
|
|
88
|
-
# Parses uppercase letters
|
|
165
|
+
# Parses uppercase letters starting at position.
|
|
89
166
|
#
|
|
90
|
-
# @param
|
|
91
|
-
# @
|
|
92
|
-
# @
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
167
|
+
# @param string [String] input string
|
|
168
|
+
# @param pos [Integer] starting position
|
|
169
|
+
# @return [Array(Integer, Integer)] decoded value and characters consumed
|
|
170
|
+
# @raise [Sashite::Cell::Errors::Argument] if parsing fails
|
|
171
|
+
private_class_method def self.parse_uppercase(string, pos)
|
|
172
|
+
byte = string.getbyte(pos)
|
|
173
|
+
unless uppercase?(byte)
|
|
174
|
+
raise Errors::Argument, Errors::Argument::Messages::UNEXPECTED_CHARACTER
|
|
175
|
+
end
|
|
96
176
|
|
|
97
|
-
|
|
98
|
-
|
|
177
|
+
chars = [byte]
|
|
178
|
+
pos += 1
|
|
99
179
|
|
|
100
|
-
|
|
180
|
+
while pos < string.length && uppercase?(string.getbyte(pos))
|
|
181
|
+
chars << string.getbyte(pos)
|
|
182
|
+
pos += 1
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
value = decode_uppercase(chars)
|
|
186
|
+
if value > Constants::MAX_INDEX_VALUE
|
|
187
|
+
raise Errors::Argument, Errors::Argument::Messages::INDEX_OUT_OF_RANGE
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
[value, chars.size]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Decodes lowercase letter bytes to an index.
|
|
194
|
+
#
|
|
195
|
+
# @param bytes [Array<Integer>] byte values
|
|
196
|
+
# @return [Integer] decoded index (0-255)
|
|
197
|
+
private_class_method def self.decode_lowercase(bytes)
|
|
198
|
+
if bytes.size == 1
|
|
199
|
+
bytes[0] - 97 # 'a' = 97
|
|
200
|
+
else
|
|
201
|
+
first = bytes[0] - 97
|
|
202
|
+
second = bytes[1] - 97
|
|
203
|
+
26 + (first * 26) + second
|
|
204
|
+
end
|
|
101
205
|
end
|
|
102
206
|
|
|
103
|
-
# Decodes letter
|
|
207
|
+
# Decodes uppercase letter bytes to an index.
|
|
104
208
|
#
|
|
105
|
-
# @param
|
|
106
|
-
# @return [Integer] decoded index
|
|
107
|
-
private_class_method def self.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
209
|
+
# @param bytes [Array<Integer>] byte values
|
|
210
|
+
# @return [Integer] decoded index (0-255)
|
|
211
|
+
private_class_method def self.decode_uppercase(bytes)
|
|
212
|
+
if bytes.size == 1
|
|
213
|
+
bytes[0] - 65 # 'A' = 65
|
|
214
|
+
else
|
|
215
|
+
first = bytes[0] - 65
|
|
216
|
+
second = bytes[1] - 65
|
|
217
|
+
26 + (first * 26) + second
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Decodes digit bytes to an index (1-based to 0-based).
|
|
222
|
+
#
|
|
223
|
+
# @param bytes [Array<Integer>] byte values
|
|
224
|
+
# @return [Integer] decoded index (0-255)
|
|
225
|
+
private_class_method def self.decode_integer(bytes)
|
|
226
|
+
value = 0
|
|
227
|
+
bytes.each do |byte|
|
|
228
|
+
value = (value * 10) + (byte - 48) # '0' = 48
|
|
229
|
+
end
|
|
230
|
+
value - 1 # Convert from 1-based to 0-based
|
|
117
231
|
end
|
|
118
232
|
end
|
|
119
233
|
end
|
data/lib/sashite/cell.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cell/errors"
|
|
4
|
+
require_relative "cell/formatter"
|
|
3
5
|
require_relative "cell/coordinate"
|
|
4
|
-
require_relative "cell/dumper"
|
|
5
6
|
require_relative "cell/parser"
|
|
6
7
|
|
|
7
8
|
module Sashite
|
|
@@ -28,11 +29,12 @@ module Sashite
|
|
|
28
29
|
#
|
|
29
30
|
# @param string [String] CELL coordinate string
|
|
30
31
|
# @return [Coordinate] parsed coordinate
|
|
31
|
-
# @raise [
|
|
32
|
+
# @raise [Sashite::Cell::Errors::Argument] if the string is not a valid CELL coordinate
|
|
32
33
|
#
|
|
33
34
|
# @example
|
|
34
35
|
# Sashite::Cell.parse("e4") # => #<Sashite::Cell::Coordinate e4>
|
|
35
|
-
# Sashite::Cell.parse("
|
|
36
|
+
# Sashite::Cell.parse("a1A") # => #<Sashite::Cell::Coordinate a1A>
|
|
37
|
+
# Sashite::Cell.parse("a0") # => raises Sashite::Cell::Errors::Argument
|
|
36
38
|
def self.parse(string)
|
|
37
39
|
Coordinate.new(*Parser.parse_to_indices(string))
|
|
38
40
|
end
|
|
@@ -41,7 +43,7 @@ module Sashite
|
|
|
41
43
|
#
|
|
42
44
|
# @param indices [Array<Integer>] 0-indexed coordinate values (0-255)
|
|
43
45
|
# @return [String] CELL coordinate string
|
|
44
|
-
# @raise [
|
|
46
|
+
# @raise [Sashite::Cell::Errors::Argument] if indices are invalid
|
|
45
47
|
#
|
|
46
48
|
# @example
|
|
47
49
|
# Sashite::Cell.format(4, 3) # => "e4"
|
|
@@ -54,11 +56,11 @@ module Sashite
|
|
|
54
56
|
#
|
|
55
57
|
# @param string [String] CELL coordinate string
|
|
56
58
|
# @return [nil]
|
|
57
|
-
# @raise [
|
|
59
|
+
# @raise [Sashite::Cell::Errors::Argument] if the string is not a valid CELL coordinate
|
|
58
60
|
#
|
|
59
61
|
# @example
|
|
60
62
|
# Sashite::Cell.validate("e4") # => nil
|
|
61
|
-
# Sashite::Cell.validate("a0") # => raises
|
|
63
|
+
# Sashite::Cell.validate("a0") # => raises Sashite::Cell::Errors::Argument
|
|
62
64
|
def self.validate(string)
|
|
63
65
|
Parser.parse_to_indices(string)
|
|
64
66
|
nil
|
|
@@ -75,7 +77,7 @@ module Sashite
|
|
|
75
77
|
def self.valid?(string)
|
|
76
78
|
validate(string)
|
|
77
79
|
true
|
|
78
|
-
rescue ::
|
|
80
|
+
rescue Errors::Argument
|
|
79
81
|
false
|
|
80
82
|
end
|
|
81
83
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sashite-cell
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
@@ -22,8 +22,12 @@ files:
|
|
|
22
22
|
- README.md
|
|
23
23
|
- lib/sashite-cell.rb
|
|
24
24
|
- lib/sashite/cell.rb
|
|
25
|
+
- lib/sashite/cell/constants.rb
|
|
25
26
|
- lib/sashite/cell/coordinate.rb
|
|
26
|
-
- lib/sashite/cell/
|
|
27
|
+
- lib/sashite/cell/errors.rb
|
|
28
|
+
- lib/sashite/cell/errors/argument.rb
|
|
29
|
+
- lib/sashite/cell/errors/argument/messages.rb
|
|
30
|
+
- lib/sashite/cell/formatter.rb
|
|
27
31
|
- lib/sashite/cell/parser.rb
|
|
28
32
|
homepage: https://github.com/sashite/cell.rb
|
|
29
33
|
licenses:
|
data/lib/sashite/cell/dumper.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Sashite
|
|
4
|
-
module Cell
|
|
5
|
-
# Formats index arrays into CELL coordinate strings.
|
|
6
|
-
#
|
|
7
|
-
# @example
|
|
8
|
-
# Dumper.indices_to_string([4, 3]) # => "e4"
|
|
9
|
-
# Dumper.indices_to_string([0, 0, 0]) # => "a1A"
|
|
10
|
-
#
|
|
11
|
-
# @api private
|
|
12
|
-
module Dumper
|
|
13
|
-
# Formats indices array to CELL string.
|
|
14
|
-
#
|
|
15
|
-
# @param indices [Array<Integer>] 0-indexed coordinate values
|
|
16
|
-
# @return [String] CELL coordinate string
|
|
17
|
-
def self.indices_to_string(indices)
|
|
18
|
-
indices.each_with_index.map do |index, i|
|
|
19
|
-
dimension = i + 1
|
|
20
|
-
case dimension % 3
|
|
21
|
-
when 1 then encode_letters(index, uppercase: false)
|
|
22
|
-
when 2 then encode_integer(index)
|
|
23
|
-
when 0 then encode_letters(index, uppercase: true)
|
|
24
|
-
end
|
|
25
|
-
end.join
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Encodes index to letter sequence (0=a, 25=z, 26=aa, ...).
|
|
29
|
-
#
|
|
30
|
-
# @param index [Integer] 0-indexed value
|
|
31
|
-
# @param uppercase [Boolean] whether to use uppercase letters
|
|
32
|
-
# @return [String] letter sequence
|
|
33
|
-
private_class_method def self.encode_letters(index, uppercase:)
|
|
34
|
-
base_char = uppercase ? "A" : "a"
|
|
35
|
-
|
|
36
|
-
if index < 26
|
|
37
|
-
(base_char.ord + index).chr
|
|
38
|
-
else
|
|
39
|
-
adjusted = index - 26
|
|
40
|
-
first = adjusted / 26
|
|
41
|
-
second = adjusted % 26
|
|
42
|
-
(base_char.ord + first).chr + (base_char.ord + second).chr
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Encodes index to number string (0=1, 1=2, ...).
|
|
47
|
-
#
|
|
48
|
-
# @param index [Integer] 0-indexed value
|
|
49
|
-
# @return [String] number string (1-indexed)
|
|
50
|
-
private_class_method def self.encode_integer(index)
|
|
51
|
-
(index + 1).to_s
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|