pgn2 1.1.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b17339791fa63da88b55a180d613c124509f7c286552fd9bb62dcea162eb3325
4
- data.tar.gz: 2283fa7d6fbf06d54bebbdb9675427c2c51743627423db6bdd91df71a8e2926f
3
+ metadata.gz: 4a5ce857277684a9f89c02d35941d7c455df2ca8a18f557985f3ca92eed414b3
4
+ data.tar.gz: 5f126a3da7a6f314748495919168336bb7b97d07e5ad0c9f2ac9b9956a8bb4be
5
5
  SHA512:
6
- metadata.gz: 311d858dca8016414e862b016fe694dff758ea575194d1a4256ebfa02e4427bc389ca291db5eb4a4cf547839e74e717aaeaed68fece0b43a1faf840148b87235
7
- data.tar.gz: a0ad909b77394c5d7982db61ff37eb4ff02b2e1241b942e48b79bf290c70f5e6e756432fed01a5041179f27546faa2edae2a3b0cd54ff9a7f13a6bb3ac95c006
6
+ metadata.gz: '079c4e3c1dd292c58fac802349c8d9d8054efa4a01288090bd0a38472029b8848a162cd4264fe83135c80d14fb9ea65d531009ddc6dbd6237e30c4f4bb9efda8'
7
+ data.tar.gz: e3ec9dbcf8dad46e7e8ec623642d468253a4ee6d5899dd9d1ac7486e60416b9069a49e34de092e08744439b9c40dce84074a6ddcaed91a584d88a65be6646b9a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 (2026-08-13)
4
+
5
+ ### Summary
6
+
7
+ Parse allocation quick win: collapse `PGN::Lexer#scan_one`'s per-token
8
+ 3-element tuple. No public API changes; serialized PGN/FEN output stays
9
+ byte-identical. See `bench/IMPROVEMENTS.md` for the per-step deltas.
10
+
11
+ ### Changed
12
+ - **Lexer hot path**: `PGN::Lexer#scan_one` now returns the matched string
13
+ directly (stashing its type/discarded flag in ivars) instead of a
14
+ 3-element `[type, m, discarded]` tuple, so the parser path (`next_token_pair`)
15
+ allocates only the single `[type, value]` array Racc requires per token.
16
+ Selected by per-line allocation profiling (`scan_one`'s tuple was the #1
17
+ parse allocation site).
18
+ - **Deferred**: the coordinate-only-board + piece-location-index work was
19
+ profiled and deferred to a single coherent board-representation rewrite
20
+ (see TODO) — coordinate-board alone measured only ~1.25× replay at medium
21
+ risk and would likely be superseded by the piece-index rewrite.
22
+ - Performance vs 1.1.0: parse-only allocations −42% objects (603537 → 347037 /
23
+ 500 games); parse+replay −18% objects (1427586 → 1170586). Replay path
24
+ unchanged. All 182 specs pass; racc-sync OK.
25
+
3
26
  ## 1.1.0 (2026-08-13)
4
27
 
5
28
  ### Summary
data/README.md CHANGED
@@ -181,12 +181,12 @@ Parser — 500 immortal games (`bench/profile_parse.rb`):
181
181
 
182
182
  | Metric | original `pgn` | pgn2 | Δ |
183
183
  |---|---:|---:|---:|
184
- | Parse-only allocations (objects) | 1248065 | 603537 | -644528 (-51.6%) |
185
- | Parse-only allocations (bytes) | 120370470 | 28257414 | -92113056 (-76.5%) |
186
- | Parse + replay allocations (objects) | 3778073 | 1427586 | -2350487 (-62.2%) |
187
- | Parse + replay allocations (bytes) | 249570048 | 78104136 | -171465912 (-68.7%) |
188
- | Parse-only throughput | 1461 ms/i | 327 ms/i | ~4.5x faster |
189
- | Parse + replay throughput | 1938 ms/i | 826 ms/i | ~2.35x faster |
184
+ | Parse-only allocations (objects) | 1248065 | 347037 | -901028 (-72.2%) |
185
+ | Parse-only allocations (bytes) | 120370470 | 17977414 | -102393056 (-85.1%) |
186
+ | Parse + replay allocations (objects) | 3778073 | 1170586 | -2607487 (-69.0%) |
187
+ | Parse + replay allocations (bytes) | 249570048 | 67804136 | -181765912 (-72.8%) |
188
+ | Parse-only throughput | 1461 ms/i | 305 ms/i | ~4.8x faster |
189
+ | Parse + replay throughput | 1938 ms/i | 816 ms/i | ~2.4x faster |
190
190
 
191
191
  What changed to get there:
192
192
 
@@ -211,6 +211,11 @@ What changed to get there:
211
211
  11. `PGN::MoveCalculator` — memoized `destination_coords`, frozen
212
212
  `ROOK_RESTRICTIONS`, empty short-circuit in `castling_restrictions`;
213
213
  `Move#pawn?` non-allocating.
214
+ 12. `PGN::Lexer#scan_one` — returns the matched string directly (stashing
215
+ type/discarded in ivars) instead of a 3-element `[type, m, discarded]`
216
+ tuple, so the parser hot path now allocates only the single `[type, value]`
217
+ array Racc requires per token. Cuts parse allocations ~42% (603537 → 347037
218
+ objects for 500 games).
214
219
 
215
220
  Public output (FEN, PGN) is byte-identical to the original gem; the full
216
221
  suite (182 examples) stays green. See `bench/IMPROVEMENTS.md` for the per-step
data/TODO.md CHANGED
@@ -10,12 +10,41 @@
10
10
 
11
11
  - Support converting a game to pgn format
12
12
  - Speed up parsing
13
- - Collapse PGN::Lexer's per-token allocation chain (Array in `scan_one` ->
14
- `Token` struct -> Array in racc's `next_token`) to fewer allocations on
15
- the hot path; keep the full `Token` (with offset/line) only for the
16
- `#tokens` spec helper, which is its only consumer.
13
+ - (done in 1.2.0) Removed `PGN::Lexer`'s per-token `Token` Struct
14
+ allocation on the parser hot path (`next_token_pair`), and collapsed
15
+ `scan_one`'s `[type, m, discarded]` tuple to a single returned string
16
+ (type/discarded stashed in ivars). The full `Token` is kept only for the
17
+ `#tokens` spec helper. Parse allocations −42% (603537 → 347037 / 500 games).
18
+ - Speed up replay via a board-representation rewrite (deferred "Approach B"):
19
+ per-line profiling shows the remaining replay allocations are architectural
20
+ — `MoveCalculator#first_piece` scan-return arrays (~5/ply, the #1 site) and
21
+ `Board#position_for` string joins (~3/ply). Do these as ONE coherent
22
+ rewrite (not separately, to avoid throwing away work):
23
+ (a) a piece-location index (piece → squares) so king/disambiguation/origin
24
+ lookups are O(1) instead of scanning 64 squares — kills `first_piece` scan
25
+ arrays AND the dominant replay compute (`valid_square?`/`at` ≈ 15/ply calls);
26
+ (b) a coordinate-only internal board (int square keys, no `"e4"` strings on
27
+ the hot path) — kills `position_for` strings + `changes` string keys.
28
+ Caveat: `change!`/`update`/`position_for`/`coordinates_for`/`squares` are
29
+ spec-tested public API, so the new representation must be additive (string
30
+ API kept). Realistic ceiling ~2× replay allocation + ~1.5–2× throughput;
31
+ medium-high risk (Board/MoveCalculator/Position/FEN). Only worth it given a
32
+ real hot-loop need (replay is already ~0.8 ms/ply).
17
33
  - Replace the right-recursive `tag_section`/`variation_list` rules in
18
34
  `pgn_parser.y` with ordinary left-recursion plus one explicit `.reverse`
19
35
  at the point each list is consumed, so the legacy whittle-order
20
36
  compatibility quirk is a single greppable line instead of implicit in
21
37
  recursion direction.
38
+ - Make `MoveText#clean_text` idempotent (or run it exactly once, at
39
+ construction) so `Game#moves=`/`#standardize_castling` doesn't need to
40
+ sniff a comment for leftover `{`/`}` to decide whether a MoveText is safe
41
+ to reuse as-is. The brace check is a bandaid for `clean_text` not fully
42
+ normalizing multi-line/nested comments in one pass; fixing that at the
43
+ source would let `moves=` reuse unconditionally.
44
+ - `MoveCalculator#destination_coords` memoizes into `@dest_coords` based on
45
+ `board`/`move`/`origin` never changing after `#initialize` — true today,
46
+ but only by convention, since `board`, `move`, `origin` are all public
47
+ `attr_accessor`s with no cache invalidation tied to their setters. If a
48
+ future caller ever mutates and reuses a `MoveCalculator` instance, this
49
+ memo goes stale silently. Either drop the public setters or invalidate
50
+ `@dest_coords` when they're used.
@@ -141,3 +141,27 @@ byte-identical (covered by `spec/lexer_spec.rb`, `spec/parser_explicit_spec.rb`,
141
141
  and the fixture round-trip in `spec/game_spec.rb`). Racc parser is in sync with
142
142
  `pgn_parser.y` (CI racc-sync check passes). No public API or serialized-output
143
143
  changes.
144
+
145
+ ## Token-array collapse (2026-08-13, v1.2.0)
146
+
147
+ Collapse `PGN::Lexer#scan_one`'s per-token 3-element `[type, m, discarded]` tuple.
148
+ `scan_one` now returns the matched string directly and stashes its type /
149
+ discarded flag in `@scan_type` / `@scan_discarded` ivars, so the parser hot path
150
+ (`next_token_pair`) allocates only the single `[type, value]` array Racc requires.
151
+ Selected by per-line allocation profiling: `scan_one`'s tuple was the #1 parse
152
+ allocation site. The coordinate-only-board + piece-location-index work was
153
+ profiled and **deferred** — the board rewrite is a larger design (see TODO), and
154
+ coordinate-board alone measured only ~1.25× replay at medium risk and would
155
+ likely be superseded by the piece-index rewrite.
156
+
157
+ ### bench/profile_parse.rb (500 immortal games)
158
+
159
+ | Metric | BEFORE | AFTER | Δ |
160
+ |---|---|---|---|
161
+ | Parse-only allocations (objects) | 603537 | 347037 | -256500 (-42.5%) |
162
+ | Parse-only allocations (bytes) | 28257414 | 17977414 | -10280000 (-36.4%) |
163
+ | Parse + replay allocations (objects) | 1427586 | 1170586 | -257000 (-18.0%) |
164
+ | Parse + replay allocations (bytes) | 78104136 | 67804136 | -10300000 (-13.2%) |
165
+
166
+ Replay (`profile_moves.rb`) is unchanged (1709 objects / 103720 bytes) — the
167
+ lexer edit does not touch the replay path. All 182 specs pass; racc-sync OK.
@@ -1,25 +1,25 @@
1
1
  Corpus: 500 copies of the immortal game
2
2
 
3
3
  === 1. Parse-only allocations (500 games) ===
4
- total_allocated objects: 603537
5
- total_allocated bytes: 28257414
4
+ total_allocated objects: 347037
5
+ total_allocated bytes: 17977414
6
6
 
7
7
  === 2. Parse + replay allocations (500 games) ===
8
- total_allocated objects: 1427586
9
- total_allocated bytes: 78104136
8
+ total_allocated objects: 1170586
9
+ total_allocated bytes: 67804136
10
10
 
11
11
  === 3. Parse-only throughput (ips) ===
12
12
  ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
13
13
  Warming up --------------------------------------
14
14
  parse 500 games 1.000 i/100ms
15
15
  Calculating -------------------------------------
16
- parse 500 games 3.64927.4%) i/s (274.08 ms/i) - 19.000 in 5.207546s
16
+ parse 500 games 4.086 0.0%) i/s (244.74 ms/i) - 21.000 in 5.139596s
17
17
 
18
18
  === 4. Parse + replay throughput (ips) ===
19
19
  ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
20
20
  Warming up --------------------------------------
21
21
  parse+replay 500 games 1.000 i/100ms
22
22
  Calculating -------------------------------------
23
- parse+replay 500 games 1.399 (± 0.0%) i/s (715.05 ms/i) - 7.000 in 5.005330s
23
+ parse+replay 500 games 1.472 (± 0.0%) i/s (679.56 ms/i) - 8.000 in 5.436470s
24
24
 
25
25
  Done. Compare this file against bench/baseline_parse.txt after optimizations.
data/lib/pgn/game.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'io/console'
2
4
 
3
5
  module PGN
@@ -11,12 +13,12 @@ module PGN
11
13
  @variations = variations
12
14
  end
13
15
 
14
- def ==(m)
15
- to_s == m.to_s
16
+ def ==(other)
17
+ to_s == other.to_s
16
18
  end
17
19
 
18
- def eql?(m)
19
- self == m
20
+ def eql?(other)
21
+ self == other
20
22
  end
21
23
 
22
24
  def hash
@@ -31,6 +33,7 @@ module PGN
31
33
  text&.gsub(/{(.*)}/, '\1')&.gsub(/\s+/, ' ')&.strip
32
34
  end
33
35
  end
36
+
34
37
  # {PGN::Game} holds all of the information about a game. It is either
35
38
  # the result of parsing a PGN file, or created by hand.
36
39
  #
@@ -57,9 +60,9 @@ module PGN
57
60
  attr_accessor :tags, :result, :pgn, :comment
58
61
  attr_reader :moves
59
62
 
60
- LEFT = /(a|\x1B\[D)\z/.freeze
61
- RIGHT = /(d|\x1B\[C)\z/.freeze
62
- EXIT = /(q|\x03)\z/.freeze
63
+ LEFT = /(a|\x1B\[D)\z/
64
+ RIGHT = /(d|\x1B\[C)\z/
65
+ EXIT = /(q|\x03)\z/
63
66
 
64
67
  # @param moves [Array<String>] a list of moves in SAN
65
68
  # @param tags [Hash<String, String>] metadata about the game
@@ -77,28 +80,8 @@ module PGN
77
80
  #
78
81
  # Standardize castling moves to use O's instead of 0's
79
82
  #
80
- # Reuse-safety invariant: when an element is already a MoveText we reuse
81
- # it directly (sharing one object between the parser's move tree and
82
- # Game#moves) whenever its comment is already fully cleaned. This is safe
83
- # because nothing mutates a MoveText after this assignment returns. We
84
- # still re-wrap when the comment carries braces: the parser's single-pass
85
- # clean_text leaves braces on multi-line/nested comments, and the second
86
- # clean_text that MoveText.new applies here is load-bearing for those
87
- # (matches the legacy whittle byte-output). Skipping it would change
88
- # serialized comments.
89
83
  def moves=(moves)
90
- @moves =
91
- moves.map do |m|
92
- if m.is_a?(String)
93
- MoveText.new(m.include?('0') ? m.gsub('0', 'O') : m)
94
- elsif m.notation.include?('0')
95
- MoveText.new(m.notation.gsub('0', 'O'), m.annotation, m.comment, m.variations)
96
- elsif m.comment.nil? || !m.comment.include?('{')
97
- m
98
- else
99
- MoveText.new(m.notation, m.annotation, m.comment, m.variations)
100
- end
101
- end
84
+ @moves = moves.map { |m| standardize_castling(m) }
102
85
  end
103
86
 
104
87
  # @return [String] a canonical PGN string for this game, ending with a
@@ -152,11 +135,11 @@ module PGN
152
135
  loop do
153
136
  puts "\e[H\e[2J"
154
137
  puts positions[index].inspect
155
- hist[0..2] = (hist[1..2] << STDIN.getch)
138
+ hist[0..2] = (hist[1..2] << $stdin.getch)
156
139
 
157
140
  case hist.join
158
141
  when LEFT
159
- index -= 1 if index > 0
142
+ index -= 1 if index.positive?
160
143
  when RIGHT
161
144
  index += 1 if index < moves.length
162
145
  when EXIT
@@ -164,5 +147,22 @@ module PGN
164
147
  end
165
148
  end
166
149
  end
150
+
151
+ private
152
+
153
+ # A MoveText is reused as-is (no new object) when its notation needs no
154
+ # '0'->'O' fix and its comment has no braces left to clean; a braced
155
+ # comment still needs MoveText.new's second clean_text pass to match the
156
+ # legacy whittle byte-output.
157
+ def standardize_castling(entry)
158
+ return MoveText.new(entry.include?('0') ? entry.gsub('0', 'O') : entry) if entry.is_a?(String)
159
+
160
+ notation = entry.notation.include?('0') ? entry.notation.gsub('0', 'O') : entry.notation
161
+ if notation.equal?(entry.notation) && (entry.comment.nil? || !entry.comment.include?('{'))
162
+ return entry
163
+ end
164
+
165
+ MoveText.new(notation, entry.annotation, entry.comment, entry.variations)
166
+ end
167
167
  end
168
168
  end
data/lib/pgn/lexer.rb CHANGED
@@ -146,29 +146,18 @@ module PGN
146
146
 
147
147
  # Returns the next {Token}, or +nil+ at end of input.
148
148
  def next_token
149
- type, value, off = scan_next
149
+ type, value = next_token_pair
150
150
  return nil unless type
151
- Token.new(type: type, value: value, offset: off, line: @line)
151
+
152
+ Token.new(type: type, value: value, offset: @last_offset, line: @line)
152
153
  end
153
154
 
154
155
  # Fast path for the parser: returns [type, value] for the next
155
156
  # non-discarded token, or +nil+ at end of input. Does not allocate a
156
- # {Token} Struct. Shares the same scanning routine and the same
157
- # +note_token+ / +advance_line+ side effects as +next_token+ (so
158
- # +game_starts+ tracking is preserved).
157
+ # {Token} Struct, and +scan_one+ returns the matched string directly
158
+ # (stashing its type/discarded flag in ivars) so the only array
159
+ # allocated per token is the [type, value] pair Racc requires.
159
160
  def next_token_pair
160
- type, value, = scan_next
161
- return nil unless type
162
- [type, value]
163
- end
164
-
165
- private
166
-
167
- # Scan to the next non-discarded token and return [type, value, offset],
168
- # or nil at end of input. Performs the exact +advance_line+ and
169
- # +note_token+ side effects that +next_token+ historically did, so
170
- # +game_starts+ (used for verbatim +Game#pgn+ slicing) stays correct.
171
- def scan_next
172
161
  until @ss.eos?
173
162
  off = @ss.pos
174
163
 
@@ -176,25 +165,33 @@ module PGN
176
165
  type, value = lit
177
166
  @ss.pos = off + 1
178
167
  note_token(type, off)
179
- return [type, value, off]
168
+ @last_offset = off
169
+ return [type, value]
180
170
  end
181
171
 
182
- type, value, discarded = scan_one
172
+ value = scan_one
183
173
  advance_line(value)
184
- next if discarded
174
+ next if @scan_discarded
185
175
 
186
- note_token(type, off)
187
- return [type, value, off]
176
+ note_token(@scan_type, off)
177
+ @last_offset = off
178
+ return [@scan_type, value]
188
179
  end
189
180
  nil
190
181
  end
191
182
 
192
- # Try each terminal rule in order; return [type, matched_string, discarded]
193
- # for the first match, or raise if nothing matches at the current position.
183
+ private
184
+
185
+ # Try each terminal rule in order; return the matched string for the
186
+ # first match (stashing its type and discarded flag in +@scan_type+ /
187
+ # +@scan_discarded+ so the caller avoids allocating a 3-element tuple),
188
+ # or raise if nothing matches at the current position.
194
189
  def scan_one
195
190
  RULES.each do |(type, re, discarded)|
196
191
  if (m = @ss.scan(re))
197
- return [type, m, discarded]
192
+ @scan_type = type
193
+ @scan_discarded = discarded
194
+ return m
198
195
  end
199
196
  end
200
197
  raise UnconsumedInputError,
@@ -118,15 +118,13 @@ module PGN
118
118
  restrict += %w[K Q]
119
119
  when 'k'
120
120
  restrict += %w[k q]
121
- when 'R'
122
- restrict << ROOK_RESTRICTIONS[origin]
123
- when 'r'
121
+ when 'R', 'r'
124
122
  restrict << ROOK_RESTRICTIONS[origin]
125
123
  end
126
124
 
127
125
  # when castling occurs
128
- restrict += %w[K Q] if %w[K Q].include? move.castle
129
- restrict += %w[k q] if %w[k q].include? move.castle
126
+ restrict += %w[K Q] if %w[K Q].include?(move.castle)
127
+ restrict += %w[k q] if %w[k q].include?(move.castle)
130
128
 
131
129
  # when a rook is taken
132
130
  restrict << 'Q' if move.destination == 'a1'
@@ -337,7 +335,7 @@ module PGN
337
335
  end
338
336
 
339
337
  def destination_coords
340
- @dest_coords ||= board.coordinates_for(move.destination)
338
+ @destination_coords ||= board.coordinates_for(move.destination)
341
339
  end
342
340
  end
343
341
  end
@@ -27,13 +27,20 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 97)
27
27
 
28
28
  private
29
29
 
30
- # Punctuation tokens are racc'd by literal; everything else is racc'd by
31
- # the upcased lexer symbol (e.g. :san_move -> :SAN_MOVE), so there's a
32
- # single source of truth for the token vocabulary: PGN::Lexer's rules.
33
- LITERAL_TOKENS = { lbracket: '[', rbracket: ']', lparen: '(', rparen: ')' }.freeze
30
+ # Punctuation tokens are racc'd by their literal character (taken from
31
+ # PGN::Lexer::LITERAL_BYTES, the single source of truth for those
32
+ # characters); everything else is racc'd by the upcased lexer symbol.
33
+ # A complete frozen lookup keeps this a plain hash read for every token
34
+ # instead of allocating two Strings (`to_s` + `upcase`) per token.
35
+ TOKEN_TRANSLATIONS = PGN::Lexer::LITERAL_BYTES.values.to_h.merge(
36
+ string: :STRING, comment: :COMMENT, game_termination: :GAME_TERMINATION,
37
+ san_move: :SAN_MOVE, nag: :NAG, move_number: :MOVE_NUMBER, tag_name: :TAG_NAME
38
+ ).freeze
34
39
 
35
40
  def translate_type(sym)
36
- LITERAL_TOKENS[sym] || sym.to_s.upcase.to_sym
41
+ TOKEN_TRANSLATIONS.fetch(sym) do
42
+ raise "PGN::Lexer produced an unknown token type: #{sym.inspect}"
43
+ end
37
44
  end
38
45
 
39
46
  # Slice the verbatim raw PGN text per game out of the original input using
data/lib/pgn/pgn_parser.y CHANGED
@@ -112,13 +112,20 @@ rule
112
112
 
113
113
  private
114
114
 
115
- # Punctuation tokens are racc'd by literal; everything else is racc'd by
116
- # the upcased lexer symbol (e.g. :san_move -> :SAN_MOVE), so there's a
117
- # single source of truth for the token vocabulary: PGN::Lexer's rules.
118
- LITERAL_TOKENS = { lbracket: '[', rbracket: ']', lparen: '(', rparen: ')' }.freeze
115
+ # Punctuation tokens are racc'd by their literal character (taken from
116
+ # PGN::Lexer::LITERAL_BYTES, the single source of truth for those
117
+ # characters); everything else is racc'd by the upcased lexer symbol.
118
+ # A complete frozen lookup keeps this a plain hash read for every token
119
+ # instead of allocating two Strings (`to_s` + `upcase`) per token.
120
+ TOKEN_TRANSLATIONS = PGN::Lexer::LITERAL_BYTES.values.to_h.merge(
121
+ string: :STRING, comment: :COMMENT, game_termination: :GAME_TERMINATION,
122
+ san_move: :SAN_MOVE, nag: :NAG, move_number: :MOVE_NUMBER, tag_name: :TAG_NAME
123
+ ).freeze
119
124
 
120
125
  def translate_type(sym)
121
- LITERAL_TOKENS[sym] || sym.to_s.upcase.to_sym
126
+ TOKEN_TRANSLATIONS.fetch(sym) do
127
+ raise "PGN::Lexer produced an unknown token type: #{sym.inspect}"
128
+ end
122
129
  end
123
130
 
124
131
  # Slice the verbatim raw PGN text per game out of the original input using
data/lib/pgn/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module PGN
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '1.2.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgn2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stacey Touset