sashite-pan 1.3.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: 12680869115791fc6fe8332162cbeaa71174b8ba5c5d74ebb531a805355dae1b
4
- data.tar.gz: 1147d462de504b726ec26337bf8bfa69b28a8d3e195dbf6b2922b9b3ccaa9cf3
3
+ metadata.gz: eec6ed9599bc268e1ef620b16fb261435b718bd6f95be8b74d82d31047caaf78
4
+ data.tar.gz: 57b17727d5e48a9a749af2962caa97e7e646ea91130fa9a44bb8eedc9690f7be
5
5
  SHA512:
6
- metadata.gz: 8bcb5c10788c3ba49269612062c6eea16251f372a176efd8c390fbe2e7967562bb4c83a27f7f70dd4f5af4af8f3a9ee33695f5d2a1394d105dff00e65043b45d
7
- data.tar.gz: 0afbe377cfe6b68a5748f74221ef4e6084c55e0439ae6d7234e6864d5403e65a3b2d19f1c5f6579869a795c994c1ccc3f8b1ef0c8bc5ed2df8734318817c0c9a
6
+ metadata.gz: 1f0b7ff44689f303b3555b3aad7e64b4960a3f1f9a51ed7830312a2110536e9ff834f448c3145271a2206d60669374cab1b1a0637317bc7d391e8a93de3c3dba
7
+ data.tar.gz: c14bcb71d0703847b7c073c30cb66ea2c785502daf21ecf08cc384576849795944f06df6142d11dde9c09fa1efca612078445a17de255af2cdd4975e076f5e2f
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2021 Cyril Kato
1
+ Copyright (c) 2014-2025 Cyril Kato
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,102 +1,296 @@
1
- # Portable Action Notation
1
+ # Pan.rb
2
2
 
3
- [![Version](https://img.shields.io/github/v/tag/sashite/pan.rb?label=Version&logo=github)](https://github.com/sashite/pan.rb/releases)
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/pan.rb?label=Version&logo=github)](https://github.com/sashite/pan.rb/tags)
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/pan.rb/main)
5
- [![CI](https://github.com/sashite/pan.rb/workflows/CI/badge.svg?branch=main)](https://github.com/sashite/pan.rb/actions?query=workflow%3Aci+branch%3Amain)
6
- [![RuboCop](https://github.com/sashite/pan.rb/workflows/RuboCop/badge.svg?branch=main)](https://github.com/sashite/pan.rb/actions?query=workflow%3Arubocop+branch%3Amain)
5
+ ![Ruby](https://github.com/sashite/pan.rb/actions/workflows/main.yml/badge.svg?branch=main)
7
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)
8
7
 
9
- A Ruby interface for data serialization in [PAN](https://developer.sashite.com/specs/portable-action-notation) format.
8
+ > **PAN** (Portable Action Notation) support for the Ruby language.
10
9
 
11
- ## Installation
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.
15
+
16
+ This gem implements the [PAN Specification v1.0.0](https://sashite.dev/documents/pan/1.0.0/), providing a Ruby interface for:
12
17
 
13
- Add this line to your application's Gemfile:
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
14
24
 
15
25
  ```ruby
26
+ # In your Gemfile
16
27
  gem "sashite-pan"
17
28
  ```
18
29
 
19
- And then execute:
30
+ Or install manually:
20
31
 
21
32
  ```sh
22
- bundle
33
+ gem install sashite-pan
23
34
  ```
24
35
 
25
- Or install it yourself as:
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
+
42
+ ```
43
+ <source>,<destination>,<piece>[,<hand_piece>]
44
+ ```
45
+
46
+ ### Multiple Actions
26
47
 
27
- ```sh
28
- gem install sashite-pan
29
48
  ```
49
+ <action1>;<action2>[;<action3>...]
50
+ ```
51
+
52
+ Where:
30
53
 
31
- ## Usage
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)
32
58
 
33
- Working with PAN can be very simple, for example:
59
+ ## Basic Usage
60
+
61
+ ### Parsing PAN Strings
62
+
63
+ Convert a PAN string into PMN format (array of action hashes):
34
64
 
35
65
  ```ruby
36
- 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
+ ```
37
87
 
38
- # Emit a PAN string
88
+ ### Safe Parsing
39
89
 
40
- actions = [
41
- [52, 36, "♙"]
42
- ]
90
+ Parse a PAN string without raising exceptions:
43
91
 
44
- Sashite::PAN.dump(*actions) # => "52,36,♙"
92
+ ```ruby
93
+ require "sashite-pan"
45
94
 
46
- # Parse a PAN string
95
+ # Valid PAN string
96
+ result = Sashite::Pan.safe_parse("e2,e4,P'")
97
+ # => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
47
98
 
48
- Sashite::PAN.parse("52,36,♙") # => [[52, 36, "♙", nil]]
99
+ # Invalid PAN string
100
+ result = Sashite::Pan.safe_parse("invalid pan string")
101
+ # => nil
49
102
  ```
50
103
 
51
- ## Example
104
+ ### Creating PAN Strings
52
105
 
53
- ### Promoting a chess pawn into a knight
106
+ Convert PMN actions (array of hashes) into a PAN string:
54
107
 
55
108
  ```ruby
56
- Sashite::PAN.dump([12, 4, ""]) # => "12,4,♘"
57
- Sashite::PAN.parse("12,4,♘") # => [[12, 4, "♘", nil]]
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"}
130
+ ]
131
+ pan_string = Sashite::Pan.dump(pmn_actions)
132
+ # => "e1,g1,K;h1,f1,R"
58
133
  ```
59
134
 
60
- ### Capturing a rook and promoting a shogi pawn
135
+ ### Safe Dumping
136
+
137
+ Create PAN strings without raising exceptions:
61
138
 
62
139
  ```ruby
63
- Sashite::PAN.dump([33, 24, "+P", "R"]) # => "33,24,+P,R"
64
- Sashite::PAN.parse("33,24,+P,R") # => [[33, 24, "+P", "R"]]
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
65
151
  ```
66
152
 
67
- ### Dropping a shogi pawn
153
+ ### Validation
154
+
155
+ Check if a string is valid PAN notation:
68
156
 
69
157
  ```ruby
70
- Sashite::PAN.dump([nil, 42, "P"]) # => "*,42,P"
71
- Sashite::PAN.parse("*,42,P") # => [[nil, 42, "P", nil]]
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)
72
167
  ```
73
168
 
74
- ***
169
+ ## Examples
75
170
 
76
- In the context of a game with several possible actions per turn, like in
77
- Western chess, more than one action could be consider like a move, and joined
78
- thanks to the [`portable_move_notation`](https://rubygems.org/gems/portable_move_notation) gem.
171
+ ### Shogi Examples
79
172
 
80
- ### Black castles on king-side
173
+ ```ruby
174
+ require "sashite-pan"
175
+
176
+ # Pawn promotion
177
+ Sashite::Pan.parse("27,18,+P")
178
+ # => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
179
+
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"}]
183
+
184
+ # Drop pawn from hand
185
+ Sashite::Pan.parse("*,27,p")
186
+ # => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
187
+ ```
188
+
189
+ ### Chess Examples
81
190
 
82
191
  ```ruby
83
- Sashite::PAN.dump([60, 62, ""], [63, 61, "♖"]) # => "60,62,♔;63,61,♖"
84
- Sashite::PAN.parse("60,62,♔;63,61,♖") # => [[60, 62, "♔", nil], [63, 61, "♖", nil]]
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
+ # ]
85
211
  ```
86
212
 
87
- ### Capturing a white chess pawn en passant
213
+ ## Integration with PMN
214
+
215
+ PAN is designed to work seamlessly with PMN (Portable Move Notation). You can easily convert between the two formats:
88
216
 
89
217
  ```ruby
90
- Sashite::PAN.dump([33, 32, ""], [32, 40, "♟"]) # => "33,32,♟;32,40,♟"
91
- Sashite::PAN.parse("33,32,♟;32,40,♟") # => [[33, 32, "", nil], [32, 40, "♟", nil]]
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"
92
239
  ```
93
240
 
94
- ## License
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
95
250
 
96
- The code is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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
258
+
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
282
+ ```
283
+
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/)
289
+
290
+ ## License
97
291
 
98
- ## 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).
99
293
 
100
- This [gem](https://rubygems.org/gems/sashite-pan) is maintained by [Sashite](https://sashite.com/).
294
+ ## About Sashité
101
295
 
102
- 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,35 +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 = nil)
15
- super()
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"]
16
39
 
17
- @src_square = src_square.nil? ? drop_char : Integer(src_square)
18
- @dst_square = Integer(dst_square)
19
- @piece_name = piece_name.to_s
20
- @piece_hand = piece_hand&.to_s
40
+ components.join(",")
21
41
  end
22
42
 
23
- def call
24
- 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"]
25
57
  end
26
58
 
27
- 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
28
66
 
29
- def action_items
30
- 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?
31
73
 
32
- [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
33
78
  end
34
79
  end
35
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,29 +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) }
12
22
  end
13
23
 
14
- def initialize(serialized_action)
15
- super()
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)
16
33
 
17
- action_args = serialized_action.split(separator)
18
- src_square = action_args.fetch(0)
19
- @src_square = src_square.eql?(drop_char) ? nil : Integer(src_square)
20
- @dst_square = Integer(action_args.fetch(1))
21
- @piece_name = action_args.fetch(2)
22
- @piece_hand = action_args.fetch(3, nil)
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
23
42
  end
24
43
 
25
- def call
26
- [src_square, dst_square, piece_name, piece_hand]
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]
66
+ end
67
+
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
74
+ end
75
+
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
27
87
  end
28
88
  end
29
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
7
  require_relative "sashite/pan"
metadata CHANGED
@@ -1,157 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pan
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.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: 2021-08-17 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-md
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-performance
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: rubocop-rake
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: rubocop-thread_safety
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
- - !ruby/object:Gem::Dependency
126
- name: simplecov
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: yard
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- description: A Ruby interface for data serialization in PAN (Portable Action Notation)
154
- 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.
155
13
  email: contact@cyril.email
156
14
  executables: []
157
15
  extensions: []
@@ -161,17 +19,20 @@ files:
161
19
  - README.md
162
20
  - lib/sashite-pan.rb
163
21
  - lib/sashite/pan.rb
164
- - lib/sashite/pan/action.rb
165
22
  - lib/sashite/pan/dumper.rb
23
+ - lib/sashite/pan/dumper/error.rb
166
24
  - lib/sashite/pan/parser.rb
167
- homepage: https://developer.sashite.com/specs/portable-action-notation
25
+ - lib/sashite/pan/parser/error.rb
26
+ homepage: https://github.com/sashite/pan.rb
168
27
  licenses:
169
28
  - MIT
170
29
  metadata:
171
30
  bug_tracker_uri: https://github.com/sashite/pan.rb/issues
172
- 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
173
33
  source_code_uri: https://github.com/sashite/pan.rb
174
- post_install_message:
34
+ specification_uri: https://sashite.dev/documents/pan/1.0.0/
35
+ rubygems_mfa_required: 'true'
175
36
  rdoc_options: []
176
37
  require_paths:
177
38
  - lib
@@ -179,15 +40,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
40
  requirements:
180
41
  - - ">="
181
42
  - !ruby/object:Gem::Version
182
- version: 2.7.0
43
+ version: 3.2.0
183
44
  required_rubygems_version: !ruby/object:Gem::Requirement
184
45
  requirements:
185
46
  - - ">="
186
47
  - !ruby/object:Gem::Version
187
48
  version: '0'
188
49
  requirements: []
189
- rubygems_version: 3.2.15
190
- signing_key:
50
+ rubygems_version: 3.6.9
191
51
  specification_version: 4
192
- summary: Data serialization in PAN format.
52
+ summary: Portable Action Notation (PAN) parser and validator for Ruby
193
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