sashite-pan 1.2.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddc5404d30aecb881216eeeda12f98c7295f8dd415302f343c8ac0d0434a7289
4
- data.tar.gz: a70d32986e8e6c1f90dedd8039562089e1f40b8c64a4dcfb518f0bfdd0592904
3
+ metadata.gz: eec6ed9599bc268e1ef620b16fb261435b718bd6f95be8b74d82d31047caaf78
4
+ data.tar.gz: 57b17727d5e48a9a749af2962caa97e7e646ea91130fa9a44bb8eedc9690f7be
5
5
  SHA512:
6
- metadata.gz: f1ec2c07c1e3ecfbd9dde636595c182178891217eeea5c2724c994b8e8010f2d8ae371e48a5399ef3682ef22b6f9c35550a83123b0e2035dd0603a00c1cf101c
7
- data.tar.gz: ca173e3c7ecd0e49e9026a1f8b5ae9b7f8375c7d893e45b20b8ca7723e10dfe8701f4b4ef837414f2436014653b2accc2a9d2fb2b9b71cf9307c88d4840b745b
6
+ metadata.gz: 1f0b7ff44689f303b3555b3aad7e64b4960a3f1f9a51ed7830312a2110536e9ff834f448c3145271a2206d60669374cab1b1a0637317bc7d391e8a93de3c3dba
7
+ data.tar.gz: c14bcb71d0703847b7c073c30cb66ea2c785502daf21ecf08cc384576849795944f06df6142d11dde9c09fa1efca612078445a17de255af2cdd4975e076f5e2f
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2020 Cyril Kato
1
+ Copyright (c) 2014-2025 Cyril Kato
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,78 +1,296 @@
1
- # Portable Action Notation
1
+ # Pan.rb
2
2
 
3
- A Ruby interface for data serialization in [PAN](https://developer.sashite.com/specs/portable-action-notation) format.
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/pan.rb?label=Version&logo=github)](https://github.com/sashite/pan.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/pan.rb/main)
5
+ ![Ruby](https://github.com/sashite/pan.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/pan.rb?label=License&logo=github)](https://github.com/sashite/pan.rb/raw/main/LICENSE.md)
4
7
 
5
- ## Installation
8
+ > **PAN** (Portable Action Notation) support for the Ruby language.
9
+
10
+ ## What is PAN?
11
+
12
+ PAN (Portable Action Notation) is a compact, string-based format for representing **executed moves** in abstract strategy board games. PAN serves as a human-readable and space-efficient alternative to PMN (Portable Move Notation), expressing the same semantic information in a condensed textual format.
13
+
14
+ While PMN uses JSON arrays to describe move sequences, PAN encodes the same information using a delimited string format that is easier to read, write, and transmit in contexts where JSON overhead is undesirable.
6
15
 
7
- Add this line to your application's Gemfile:
16
+ This gem implements the [PAN Specification v1.0.0](https://sashite.dev/documents/pan/1.0.0/), providing a Ruby interface for:
17
+
18
+ - Converting between PAN strings and PMN format
19
+ - Parsing PAN strings into structured move data
20
+ - Creating PAN strings from move components
21
+ - Validating PAN strings according to the specification
22
+
23
+ ## Installation
8
24
 
9
25
  ```ruby
10
- gem 'sashite-pan'
26
+ # In your Gemfile
27
+ gem "sashite-pan"
28
+ ```
29
+
30
+ Or install manually:
31
+
32
+ ```sh
33
+ gem install sashite-pan
34
+ ```
35
+
36
+ ## PAN Format
37
+
38
+ A PAN string represents one or more **actions** that constitute a complete move in a game. The format structure is:
39
+
40
+ ### Single Action
41
+
11
42
  ```
43
+ <source>,<destination>,<piece>[,<hand_piece>]
44
+ ```
45
+
46
+ ### Multiple Actions
12
47
 
13
- And then execute:
48
+ ```
49
+ <action1>;<action2>[;<action3>...]
50
+ ```
14
51
 
15
- $ bundle
52
+ Where:
16
53
 
17
- Or install it yourself as:
54
+ - **source**: Origin square label, or `*` for drops from hand
55
+ - **destination**: Target square label
56
+ - **piece**: Piece being moved (PNN format with optional modifiers)
57
+ - **hand_piece**: Optional piece added to mover's hand (captures, promotions)
18
58
 
19
- $ gem install sashite-pan
59
+ ## Basic Usage
20
60
 
21
- ## Usage
61
+ ### Parsing PAN Strings
22
62
 
23
- Working with PAN can be very simple, for example:
63
+ Convert a PAN string into PMN format (array of action hashes):
24
64
 
25
65
  ```ruby
26
- require 'sashite/pan'
66
+ require "sashite-pan"
67
+
68
+ # Simple move
69
+ result = Sashite::Pan.parse("27,18,+P")
70
+ # => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
71
+
72
+ # Capture with hand piece
73
+ result = Sashite::Pan.parse("36,27,B,P")
74
+ # => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
75
+
76
+ # Drop from hand
77
+ result = Sashite::Pan.parse("*,27,p")
78
+ # => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
79
+
80
+ # Multiple actions (castling)
81
+ result = Sashite::Pan.parse("e1,g1,K;h1,f1,R")
82
+ # => [
83
+ # {"src_square"=>"e1", "dst_square"=>"g1", "piece_name"=>"K"},
84
+ # {"src_square"=>"h1", "dst_square"=>"f1", "piece_name"=>"R"}
85
+ # ]
86
+ ```
87
+
88
+ ### Safe Parsing
27
89
 
28
- # Emit a PAN string
90
+ Parse a PAN string without raising exceptions:
91
+
92
+ ```ruby
93
+ require "sashite-pan"
29
94
 
30
- actions = [
31
- [52, 36, '♙', nil]
95
+ # Valid PAN string
96
+ result = Sashite::Pan.safe_parse("e2,e4,P'")
97
+ # => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
98
+
99
+ # Invalid PAN string
100
+ result = Sashite::Pan.safe_parse("invalid pan string")
101
+ # => nil
102
+ ```
103
+
104
+ ### Creating PAN Strings
105
+
106
+ Convert PMN actions (array of hashes) into a PAN string:
107
+
108
+ ```ruby
109
+ require "sashite-pan"
110
+
111
+ # Simple move
112
+ pmn_actions = [{"src_square" => "27", "dst_square" => "18", "piece_name" => "+P"}]
113
+ pan_string = Sashite::Pan.dump(pmn_actions)
114
+ # => "27,18,+P"
115
+
116
+ # Capture with hand piece
117
+ pmn_actions = [{"src_square" => "36", "dst_square" => "27", "piece_name" => "B", "piece_hand" => "P"}]
118
+ pan_string = Sashite::Pan.dump(pmn_actions)
119
+ # => "36,27,B,P"
120
+
121
+ # Drop from hand
122
+ pmn_actions = [{"src_square" => nil, "dst_square" => "27", "piece_name" => "p"}]
123
+ pan_string = Sashite::Pan.dump(pmn_actions)
124
+ # => "*,27,p"
125
+
126
+ # Multiple actions (castling)
127
+ pmn_actions = [
128
+ {"src_square" => "e1", "dst_square" => "g1", "piece_name" => "K"},
129
+ {"src_square" => "h1", "dst_square" => "f1", "piece_name" => "R"}
32
130
  ]
131
+ pan_string = Sashite::Pan.dump(pmn_actions)
132
+ # => "e1,g1,K;h1,f1,R"
133
+ ```
33
134
 
34
- Sashite::PAN.dump(*actions) # => '52,36,♙'
135
+ ### Safe Dumping
35
136
 
36
- # Parse a PAN string
137
+ Create PAN strings without raising exceptions:
37
138
 
38
- Sashite::PAN.parse('52,36,♙') # => [[52, 36, '♙', nil]]
139
+ ```ruby
140
+ require "sashite-pan"
141
+
142
+ # Valid PMN data
143
+ pmn_actions = [{"src_square" => "e2", "dst_square" => "e4", "piece_name" => "P"}]
144
+ result = Sashite::Pan.safe_dump(pmn_actions)
145
+ # => "e2,e4,P"
146
+
147
+ # Invalid PMN data
148
+ invalid_data = [{"invalid" => "data"}]
149
+ result = Sashite::Pan.safe_dump(invalid_data)
150
+ # => nil
151
+ ```
152
+
153
+ ### Validation
154
+
155
+ Check if a string is valid PAN notation:
156
+
157
+ ```ruby
158
+ require "sashite-pan"
159
+
160
+ Sashite::Pan.valid?("27,18,+P") # => true
161
+ Sashite::Pan.valid?("*,27,p") # => true
162
+ Sashite::Pan.valid?("e1,g1,K;h1,f1,R") # => true
163
+
164
+ Sashite::Pan.valid?("") # => false
165
+ Sashite::Pan.valid?("invalid") # => false
166
+ Sashite::Pan.valid?("27,18") # => false (missing piece)
39
167
  ```
40
168
 
41
169
  ## Examples
42
170
 
171
+ ### Shogi Examples
172
+
43
173
  ```ruby
44
- # Black castles on king-side
174
+ require "sashite-pan"
45
175
 
46
- Sashite::PAN.dump([60, 62, '♔', nil], [63, 61, '♖', nil]) # => '60,62,♔;63,61,♖'
47
- Sashite::PAN.parse('60,62,♔;63,61,♖') # => [[60, 62, '♔', nil], [63, 61, '♖', nil]]
176
+ # Pawn promotion
177
+ Sashite::Pan.parse("27,18,+P")
178
+ # => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
48
179
 
49
- # Promoting a chess pawn into a knight
180
+ # Bishop captures promoted pawn
181
+ Sashite::Pan.parse("36,27,B,P")
182
+ # => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
50
183
 
51
- Sashite::PAN.dump([12, 4, '♘', nil]) # => '12,4,♘'
52
- Sashite::PAN.parse('12,4,♘') # => [[12, 4, '♘', nil]]
184
+ # Drop pawn from hand
185
+ Sashite::Pan.parse("*,27,p")
186
+ # => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
187
+ ```
53
188
 
54
- # Capturing a rook and promoting a shogi pawn
189
+ ### Chess Examples
55
190
 
56
- Sashite::PAN.dump([33, 24, '+P', 'R']) # => '33,24,+P,R'
57
- Sashite::PAN.parse('33,24,+P,R') # => [[33, 24, '+P', 'R']]
191
+ ```ruby
192
+ require "sashite-pan"
193
+
194
+ # Kingside castling
195
+ Sashite::Pan.parse("e1,g1,K;h1,f1,R")
196
+ # => [
197
+ # {"src_square"=>"e1", "dst_square"=>"g1", "piece_name"=>"K"},
198
+ # {"src_square"=>"h1", "dst_square"=>"f1", "piece_name"=>"R"}
199
+ # ]
200
+
201
+ # Pawn with state modifier (can be captured en passant)
202
+ Sashite::Pan.parse("e2,e4,P'")
203
+ # => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
204
+
205
+ # En passant capture (multi-step)
206
+ Sashite::Pan.parse("d4,e3,p;e3,e4,p")
207
+ # => [
208
+ # {"src_square"=>"d4", "dst_square"=>"e3", "piece_name"=>"p"},
209
+ # {"src_square"=>"e3", "dst_square"=>"e4", "piece_name"=>"p"}
210
+ # ]
211
+ ```
58
212
 
59
- # Dropping a shogi pawn
213
+ ## Integration with PMN
60
214
 
61
- Sashite::PAN.dump([nil, 42, 'P', nil]) # => '*,42,P'
62
- Sashite::PAN.parse('*,42,P') # => [[nil, 42, 'P', nil]]
215
+ PAN is designed to work seamlessly with PMN (Portable Move Notation). You can easily convert between the two formats:
63
216
 
64
- # Capturing a white chess pawn en passant
217
+ ```ruby
218
+ require "sashite-pan"
219
+ require "portable_move_notation"
220
+
221
+ # Start with a PAN string
222
+ pan_string = "e2,e4,P';d7,d5,p"
223
+
224
+ # Convert to PMN format
225
+ pmn_actions = Sashite::Pan.parse(pan_string)
226
+ # => [
227
+ # {"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"},
228
+ # {"src_square"=>"d7", "dst_square"=>"d5", "piece_name"=>"p"}
229
+ # ]
230
+
231
+ # Use with PMN library
232
+ move = PortableMoveNotation::Move.new(*pmn_actions.map { |action|
233
+ PortableMoveNotation::Action.new(**action.transform_keys(&:to_sym))
234
+ })
235
+
236
+ # Convert back to PAN
237
+ new_pan_string = Sashite::Pan.dump(pmn_actions)
238
+ # => "e2,e4,P';d7,d5,p"
239
+ ```
240
+
241
+ ## Use Cases
242
+
243
+ PAN is optimal for:
244
+
245
+ - **Move logging and game records**: Compact storage of game moves
246
+ - **Network transmission**: Efficient move data transmission
247
+ - **Command-line interfaces**: Human-readable move input/output
248
+ - **Quick manual entry**: Easy to type and edit move sequences
249
+ - **Storage optimization**: Space-efficient alternative to JSON
250
+
251
+ PMN is optimal for:
252
+
253
+ - **Programmatic analysis**: Complex move processing and validation
254
+ - **JSON-based systems**: Direct integration with JSON APIs
255
+ - **Structured data processing**: Schema validation and type checking
256
+
257
+ ## Properties of PAN
65
258
 
66
- Sashite::PAN.dump([33, 32, '♟', nil], [32, 40, '♟', nil]) # => '33,32,♟;32,40,♟'
67
- Sashite::PAN.parse('33,32,♟;32,40,♟') # => [[33, 32, '♟', nil], [32, 40, '♟', nil]]
259
+ - **Rule-agnostic**: PAN does not encode legality, validity, or game-specific conditions
260
+ - **Space-efficient**: Significantly more compact than equivalent JSON representation
261
+ - **Human-readable**: Easy to read, write, and understand
262
+ - **Lossless conversion**: Perfect bidirectional conversion with PMN format
263
+
264
+ ## Error Handling
265
+
266
+ The library provides detailed error messages for invalid input:
267
+
268
+ ```ruby
269
+ require "sashite-pan"
270
+
271
+ begin
272
+ Sashite::Pan.parse("invalid,pan") # Missing piece component
273
+ rescue Sashite::Pan::Parser::Error => e
274
+ puts e.message # => "Action must have at least 3 components (source, destination, piece)"
275
+ end
276
+
277
+ begin
278
+ Sashite::Pan.dump([{"invalid" => "data"}]) # Missing required fields
279
+ rescue Sashite::Pan::Dumper::Error => e
280
+ puts e.message # => "Action must have dst_square"
281
+ end
68
282
  ```
69
283
 
70
- ## License
284
+ ## Documentation
285
+
286
+ - [Official PAN Specification](https://sashite.dev/documents/pan/1.0.0/)
287
+ - [API Documentation](https://rubydoc.info/github/sashite/pan.rb/main)
288
+ - [PMN Specification](https://sashite.dev/documents/pmn/1.0.0/)
71
289
 
72
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
290
+ ## License
73
291
 
74
- ## About Sashite
292
+ The [gem](https://rubygems.org/gems/sashite-pan) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
75
293
 
76
- The `sashite-pan` gem is maintained by [Sashite](https://sashite.com/).
294
+ ## About Sashité
77
295
 
78
- With some [lines of code](https://github.com/sashite/), let's share the beauty of Chinese, Japanese and Western cultures through the game of chess!
296
+ This project is maintained by [Sashité](https://sashite.com/) promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Pan
5
+ module Dumper
6
+ # Error raised when PAN dumping fails
7
+ class Error < ::StandardError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,33 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'action'
3
+ require_relative "dumper/error"
4
4
 
5
5
  module Sashite
6
- module PAN
7
- # Dumper class
8
- class Dumper < Action
9
- def self.call(*actions)
10
- actions.map { |action_items| new(*action_items).call }
11
- .join(separator)
6
+ module Pan
7
+ # Dumper for converting PMN format to PAN strings
8
+ module Dumper
9
+ # Convert PMN actions to PAN string
10
+ #
11
+ # @param pmn_actions [Array<Hash>] Array of PMN action objects
12
+ # @return [String] PAN string representation
13
+ # @raise [Dumper::Error] If the PMN data is invalid
14
+ def self.call(pmn_actions)
15
+ raise Dumper::Error, "PMN actions cannot be nil" if pmn_actions.nil?
16
+ raise Dumper::Error, "PMN actions cannot be empty" if pmn_actions.empty?
17
+ raise Dumper::Error, "PMN actions must be an array" unless pmn_actions.is_a?(::Array)
18
+
19
+ pmn_actions.map { |action| dump_action(action) }.join(";")
12
20
  end
13
21
 
14
- def initialize(src_square, dst_square, piece_name, piece_hand)
15
- @src_square = src_square.nil? ? drop_char : Integer(src_square)
16
- @dst_square = Integer(dst_square)
17
- @piece_name = piece_name.to_s
18
- @piece_hand = piece_hand&.to_s
22
+ private
23
+
24
+ # Convert a single PMN action to PAN format
25
+ #
26
+ # @param action [Hash] PMN action object
27
+ # @return [String] PAN action string
28
+ # @raise [Dumper::Error] If the action is invalid
29
+ def self.dump_action(action)
30
+ validate_pmn_action(action)
31
+
32
+ components = [
33
+ dump_source_square(action["src_square"]),
34
+ action["dst_square"],
35
+ action["piece_name"]
36
+ ]
37
+
38
+ components << action["piece_hand"] if action["piece_hand"]
39
+
40
+ components.join(",")
19
41
  end
20
42
 
21
- def call
22
- action_items.join(separator)
43
+ # Validate PMN action structure
44
+ #
45
+ # @param action [Hash] PMN action to validate
46
+ # @raise [Dumper::Error] If action is invalid
47
+ def self.validate_pmn_action(action)
48
+ raise Dumper::Error, "Action must be a Hash" unless action.is_a?(::Hash)
49
+ raise Dumper::Error, "Action must have dst_square" unless action.key?("dst_square")
50
+ raise Dumper::Error, "Action must have piece_name" unless action.key?("piece_name")
51
+
52
+ raise Dumper::Error, "dst_square cannot be nil or empty" if action["dst_square"].nil? || action["dst_square"].empty?
53
+ raise Dumper::Error, "piece_name cannot be nil or empty" if action["piece_name"].nil? || action["piece_name"].empty?
54
+
55
+ validate_piece_identifier(action["piece_name"])
56
+ validate_piece_identifier(action["piece_hand"]) if action["piece_hand"]
23
57
  end
24
58
 
25
- private
59
+ # Convert source square, handling drops
60
+ #
61
+ # @param src_square [String, nil] Source square or nil for drop
62
+ # @return [String] "*" for drops, otherwise the square identifier
63
+ def self.dump_source_square(src_square)
64
+ src_square.nil? ? "*" : src_square
65
+ end
26
66
 
27
- def action_items
28
- return [src_square, dst_square, piece_name] if piece_hand.nil?
67
+ # Validate piece identifier follows PNN specification
68
+ #
69
+ # @param piece [String] Piece identifier to validate
70
+ # @raise [Dumper::Error] If piece identifier is invalid
71
+ def self.validate_piece_identifier(piece)
72
+ return if piece.nil?
29
73
 
30
- [src_square, dst_square, piece_name, piece_hand]
74
+ # PNN pattern: optional prefix (+/-), letter (a-z/A-Z), optional suffix (')
75
+ unless piece.match?(/\A[-+]?[a-zA-Z][']?\z/)
76
+ raise Dumper::Error, "Invalid piece identifier: #{piece}"
77
+ end
31
78
  end
32
79
  end
33
80
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Pan
5
+ module Parser
6
+ # Error raised when PAN parsing fails
7
+ class Error < ::StandardError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,27 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'action'
3
+ require_relative "parser/error"
4
4
 
5
5
  module Sashite
6
- module PAN
7
- # Parser class
8
- class Parser < Action
9
- def self.call(serialized_move)
10
- serialized_move.split(separator)
11
- .map { |serialized_action| new(serialized_action).call }
6
+ module Pan
7
+ # Parser for Portable Action Notation (PAN) strings
8
+ module Parser
9
+ # Parse a PAN string into PMN format
10
+ #
11
+ # @param pan_string [String] The PAN string to parse
12
+ # @return [Array<Hash>] Array of PMN action objects
13
+ # @raise [Parser::Error] If the PAN string is invalid
14
+ def self.call(pan_string)
15
+ raise Parser::Error, "PAN string cannot be nil" if pan_string.nil?
16
+ raise Parser::Error, "PAN string cannot be empty" if pan_string.empty?
17
+
18
+ actions = pan_string.split(";").map(&:strip)
19
+ raise Parser::Error, "No actions found" if actions.empty?
20
+
21
+ actions.map { |action| parse_action(action) }
22
+ end
23
+
24
+ private
25
+
26
+ # Parse a single action string into a PMN action hash
27
+ #
28
+ # @param action_string [String] Single action in PAN format
29
+ # @return [Hash] PMN action object
30
+ # @raise [Parser::Error] If the action is invalid
31
+ def self.parse_action(action_string)
32
+ components = action_string.split(",").map(&:strip)
33
+
34
+ validate_action_components(components)
35
+
36
+ {
37
+ "src_square" => parse_source_square(components[0]),
38
+ "dst_square" => components[1],
39
+ "piece_name" => components[2],
40
+ "piece_hand" => components[3] || nil
41
+ }.compact
42
+ end
43
+
44
+ # Validate action components structure
45
+ #
46
+ # @param components [Array<String>] Components of the action
47
+ # @raise [Parser::Error] If components are invalid
48
+ def self.validate_action_components(components)
49
+ case components.length
50
+ when 0, 1, 2
51
+ raise Parser::Error, "Action must have at least 3 components (source, destination, piece)"
52
+ when 3, 4
53
+ # Valid number of components
54
+ else
55
+ raise Parser::Error, "Action cannot have more than 4 components"
56
+ end
57
+
58
+ components.each_with_index do |component, index|
59
+ if component.nil? || component.empty?
60
+ raise Parser::Error, "Component #{index} cannot be empty"
61
+ end
62
+ end
63
+
64
+ validate_piece_identifier(components[2])
65
+ validate_piece_identifier(components[3]) if components[3]
12
66
  end
13
67
 
14
- def initialize(serialized_action)
15
- action_args = serialized_action.split(separator)
16
- src_square = action_args.fetch(0)
17
- @src_square = src_square.eql?(drop_char) ? nil : Integer(src_square)
18
- @dst_square = Integer(action_args.fetch(1))
19
- @piece_name = action_args.fetch(2)
20
- @piece_hand = action_args.fetch(3, nil)
68
+ # Parse source square, handling drop notation
69
+ #
70
+ # @param source [String] Source square or "*" for drop
71
+ # @return [String, nil] Square identifier or nil for drops
72
+ def self.parse_source_square(source)
73
+ source == "*" ? nil : source
21
74
  end
22
75
 
23
- def call
24
- [src_square, dst_square, piece_name, piece_hand]
76
+ # Validate piece identifier follows PNN specification
77
+ #
78
+ # @param piece [String] Piece identifier to validate
79
+ # @raise [Parser::Error] If piece identifier is invalid
80
+ def self.validate_piece_identifier(piece)
81
+ return if piece.nil?
82
+
83
+ # PNN pattern: optional prefix (+/-), letter (a-z/A-Z), optional suffix (')
84
+ unless piece.match?(/\A[-+]?[a-zA-Z][']?\z/)
85
+ raise Parser::Error, "Invalid piece identifier: #{piece}"
86
+ end
25
87
  end
26
88
  end
27
89
  end
data/lib/sashite/pan.rb CHANGED
@@ -1,17 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "pan/dumper"
4
+ require_relative "pan/parser"
5
+
3
6
  module Sashite
4
7
  # The PAN (Portable Action Notation) module
5
- module PAN
6
- def self.dump(*actions)
7
- Dumper.call(*actions)
8
+ module Pan
9
+ # Main interface for PAN operations
10
+ module_function
11
+
12
+ # Parse a PAN string into PMN format
13
+ #
14
+ # @param pan_string [String] The PAN string to parse
15
+ # @return [Array<Hash>] Array of PMN action objects
16
+ # @raise [Parser::Error] If the PAN string is invalid
17
+ def parse(pan_string)
18
+ Parser.call(pan_string)
8
19
  end
9
20
 
10
- def self.parse(string)
11
- Parser.call(string)
21
+ # Convert PMN actions to PAN string
22
+ #
23
+ # @param pmn_actions [Array<Hash>] Array of PMN action objects
24
+ # @return [String] PAN string representation
25
+ # @raise [Dumper::Error] If the PMN data is invalid
26
+ def dump(pmn_actions)
27
+ Dumper.call(pmn_actions)
28
+ end
29
+
30
+ # Validate a PAN string without raising exceptions
31
+ #
32
+ # @param pan_string [String] The PAN string to validate
33
+ # @return [Boolean] True if valid, false otherwise
34
+ def valid?(pan_string)
35
+ parse(pan_string)
36
+ true
37
+ rescue Parser::Error
38
+ false
39
+ end
40
+
41
+ # Parse a PAN string without raising exceptions
42
+ #
43
+ # @param pan_string [String] The PAN string to parse
44
+ # @return [Array<Hash>, nil] Array of PMN actions or nil if invalid
45
+ def safe_parse(pan_string)
46
+ parse(pan_string)
47
+ rescue Parser::Error
48
+ nil
49
+ end
50
+
51
+ # Convert PMN actions to PAN string without raising exceptions
52
+ #
53
+ # @param pmn_actions [Array<Hash>] Array of PMN action objects
54
+ # @return [String, nil] PAN string or nil if invalid
55
+ def safe_dump(pmn_actions)
56
+ dump(pmn_actions)
57
+ rescue Dumper::Error
58
+ nil
12
59
  end
13
60
  end
14
61
  end
15
-
16
- require_relative 'pan/dumper'
17
- require_relative 'pan/parser'
data/lib/sashite-pan.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sashite namespace
4
- module Sashite; end
3
+ # Sashité namespace
4
+ module Sashite
5
+ end
5
6
 
6
- require_relative 'sashite/pan'
7
+ require_relative "sashite/pan"
metadata CHANGED
@@ -1,129 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pan
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2020-07-05 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: awesome_print
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: byebug
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rake
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rubocop-performance
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rubocop-thread_safety
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: simplecov
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: yard
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- description: A Ruby interface for data serialization in PAN (Portable Action Notation)
126
- format.
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A Ruby implementation of the Portable Action Notation (PAN) specification.
127
13
  email: contact@cyril.email
128
14
  executables: []
129
15
  extensions: []
@@ -133,17 +19,20 @@ files:
133
19
  - README.md
134
20
  - lib/sashite-pan.rb
135
21
  - lib/sashite/pan.rb
136
- - lib/sashite/pan/action.rb
137
22
  - lib/sashite/pan/dumper.rb
23
+ - lib/sashite/pan/dumper/error.rb
138
24
  - lib/sashite/pan/parser.rb
139
- homepage: https://developer.sashite.com/specs/portable-action-notation
25
+ - lib/sashite/pan/parser/error.rb
26
+ homepage: https://github.com/sashite/pan.rb
140
27
  licenses:
141
28
  - MIT
142
29
  metadata:
143
30
  bug_tracker_uri: https://github.com/sashite/pan.rb/issues
144
- documentation_uri: https://rubydoc.info/gems/sashite-pan/index
31
+ documentation_uri: https://rubydoc.info/github/sashite/pan.rb/main
32
+ homepage_uri: https://github.com/sashite/pan.rb
145
33
  source_code_uri: https://github.com/sashite/pan.rb
146
- post_install_message:
34
+ specification_uri: https://sashite.dev/documents/pan/1.0.0/
35
+ rubygems_mfa_required: 'true'
147
36
  rdoc_options: []
148
37
  require_paths:
149
38
  - lib
@@ -151,15 +40,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
151
40
  requirements:
152
41
  - - ">="
153
42
  - !ruby/object:Gem::Version
154
- version: '0'
43
+ version: 3.2.0
155
44
  required_rubygems_version: !ruby/object:Gem::Requirement
156
45
  requirements:
157
46
  - - ">="
158
47
  - !ruby/object:Gem::Version
159
48
  version: '0'
160
49
  requirements: []
161
- rubygems_version: 3.1.2
162
- signing_key:
50
+ rubygems_version: 3.6.9
163
51
  specification_version: 4
164
- summary: Data serialization in PAN format.
52
+ summary: Portable Action Notation (PAN) parser and validator for Ruby
165
53
  test_files: []
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module PAN
5
- # Action class
6
- class Action
7
- attr_reader :src_square, :dst_square, :piece_name, :piece_hand
8
-
9
- private_class_method def self.separator
10
- ';'
11
- end
12
-
13
- private
14
-
15
- def separator
16
- ','
17
- end
18
-
19
- def drop_char
20
- '*'
21
- end
22
- end
23
- end
24
- end