@@ -172,76 +172,47 @@ public boolean hasNamedFields() {
172
172
}
173
173
174
174
private enum ParseState {
175
- INIT , LCURLY , RCURLY , FIELD , FLAG , FLAG_CHARACTER , FORMAT , FORMAT_LCURLY , FORMAT_FIELD
175
+ INIT , RCURLY
176
176
}
177
-
178
177
private static class StrFormatParser {
179
178
private boolean hasManualNumbering = false ;
180
179
private boolean hasAutoNumbering = false ;
181
180
private int autoNumberingPos = 0 ;
182
181
183
- private String currentFieldName = null ;
184
- private String nestedFieldName = null ;
185
182
private ParseState state = ParseState .INIT ;
186
183
private int nesting = 0 ;
187
184
private List <ReplacementField > result ;
188
185
189
186
private Consumer <String > issueReporter ;
190
187
private String value ;
191
- private Matcher fieldContentMatcher ;
188
+ private int pos = 0 ;
192
189
193
190
public StrFormatParser (Consumer <String > issueReporter , String value ) {
194
191
this .issueReporter = issueReporter ;
195
192
this .value = value ;
196
- this .fieldContentMatcher = FORMAT_FIELD_PATTERN . matcher ( this . value ) ;
193
+ this .pos = 0 ;
197
194
}
198
195
196
+
199
197
public Optional <StringFormat > parse () {
200
- int pos = 0 ;
198
+ pos = 0 ;
201
199
result = new ArrayList <>();
202
200
203
201
while (pos < value .length ()) {
204
202
char current = value .charAt (pos );
205
203
switch (state ) {
206
- case INIT :
207
- pos = parseInitial (current , pos );
208
- break ;
209
- case LCURLY :
210
- pos = parseFieldName (current , pos );
211
- break ;
212
- case FIELD :
213
- if (!tryParseField (current )) {
204
+ case INIT -> {
205
+ if (!tryParsingInitial (current )) {
214
206
return Optional .empty ();
215
207
}
216
- break ;
217
- case RCURLY :
218
- if (current == '}' ) {
219
- state = ParseState .INIT ;
220
- }
221
- break ;
222
- case FLAG :
223
- if (FORMAT_VALID_CONVERSION_FLAGS .indexOf (current ) == -1 ) {
224
- issueReporter .accept (String .format ("Fix this formatted string's syntax; !%c is not a valid conversion flag." , current ));
208
+ }
209
+ case RCURLY -> {
210
+ if (current != '}' ) {
211
+ issueReporter .accept (SYNTAX_ERROR_MESSAGE );
225
212
return Optional .empty ();
226
213
}
227
- state = ParseState .FLAG_CHARACTER ;
228
- break ;
229
- case FLAG_CHARACTER :
230
- if (!tryParseFlagCharacter (current )) {
231
- return Optional .empty ();
232
- }
233
- break ;
234
- case FORMAT :
235
- parseFormatSpecifier (current );
236
- break ;
237
- case FORMAT_LCURLY :
238
- pos = parseFormatCurly (pos );
239
- break ;
240
- case FORMAT_FIELD :
241
- if (!tryParseFormatSpecifierField (current )) {
242
- return Optional .empty ();
243
- }
244
- break ;
214
+ state = ParseState .INIT ;
215
+ }
245
216
}
246
217
247
218
pos += 1 ;
@@ -269,31 +240,9 @@ private boolean checkParserState() {
269
240
return true ;
270
241
}
271
242
272
- private boolean tryParseFormatSpecifierField (char current ) {
273
- if (current != '}' ) {
274
- issueReporter .accept (SYNTAX_ERROR_MESSAGE );
275
- return false ;
276
- }
277
-
278
- result .add (createField (nestedFieldName ));
279
- nesting --;
280
- state = ParseState .FORMAT ;
281
- return true ;
282
- }
283
-
284
- private int parseFormatCurly (int pos ) {
285
- if (fieldContentMatcher .region (pos , value .length ()).find ()) {
286
- // This should always match (if nothing else, an empty string), but be defensive
287
- state = ParseState .FORMAT_FIELD ;
288
- nestedFieldName = fieldContentMatcher .group ("name" );
289
- pos = fieldContentMatcher .end () - 1 ;
290
- }
291
- return pos ;
292
- }
293
-
294
- private int parseInitial (char current , int pos ) {
243
+ private boolean tryParsingInitial (char current ) {
295
244
if (current == '{' ) {
296
- state = ParseState . LCURLY ;
245
+ return tryParsingField () ;
297
246
} else if (current == '}' ) {
298
247
state = ParseState .RCURLY ;
299
248
} else if (current == '\\' ) {
@@ -304,29 +253,134 @@ private int parseInitial(char current, int pos) {
304
253
}
305
254
}
306
255
256
+ return true ;
257
+ }
258
+
259
+ private boolean tryParsingField () {
260
+ FieldParser fieldParser = new FieldParser (this , value .substring (pos ), 0 );
261
+ boolean successful = fieldParser .tryParse ();
262
+ this .pos += fieldParser .getPos () - 1 ;
263
+ return successful ;
264
+ }
265
+
266
+
267
+ public void reportIssue (String issue ) {
268
+ issueReporter .accept (issue );
269
+ }
270
+
271
+ public void addField (@ Nullable String name ) {
272
+ result .add (createField (name ));
273
+ }
274
+
275
+ private ReplacementField createField (@ Nullable String name ) {
276
+ if (name == null ) {
277
+ hasAutoNumbering = true ;
278
+ int currentPos = autoNumberingPos ;
279
+ autoNumberingPos ++;
280
+ return new PositionalField (DO_NOTHING_VALIDATOR , currentPos );
281
+ } else if (FORMAT_NUMBER_PATTERN .matcher (name ).find ()) {
282
+ hasManualNumbering = true ;
283
+ return new PositionalField (DO_NOTHING_VALIDATOR , Integer .parseInt (name ));
284
+ } else {
285
+ return new NamedField (DO_NOTHING_VALIDATOR , name );
286
+ }
287
+ }
288
+
289
+ }
290
+
291
+ private enum FieldParseState {
292
+ LCURLY , FIELD , FLAG , FLAG_CHARACTER , FORMAT , FINISHED
293
+ }
294
+ private static class FieldParser {
295
+ private StrFormatParser parent ;
296
+
297
+ private String currentFieldName = null ;
298
+ private FieldParseState state = FieldParseState .LCURLY ;
299
+ private int nesting ;
300
+
301
+ private String value ;
302
+ private int pos ;
303
+ private Matcher fieldContentMatcher ;
304
+
305
+ public FieldParser (StrFormatParser parent , String value , int nesting ) {
306
+ this .parent = parent ;
307
+ this .value = value ;
308
+ this .pos = 1 ;
309
+ this .fieldContentMatcher = FORMAT_FIELD_PATTERN .matcher (this .value );
310
+ this .nesting = nesting ;
311
+ }
312
+
313
+ public int getPos () {
307
314
return pos ;
308
315
}
309
316
310
- private void parseFormatSpecifier (char current ) {
317
+ public boolean tryParse () {
318
+ pos = 1 ;
319
+
320
+ while (pos < value .length () && state != FieldParseState .FINISHED ) {
321
+ char current = value .charAt (pos );
322
+ boolean successful = switch (state ) {
323
+ case LCURLY -> {
324
+ pos = parseFieldName (current , pos );
325
+ yield true ;
326
+ }
327
+ case FIELD -> tryParseField (current );
328
+ case FLAG -> tryParseFlag (current );
329
+ case FLAG_CHARACTER -> tryParseFlagCharacter (current );
330
+ case FORMAT -> tryParseFormatSpecifier (current );
331
+ case FINISHED -> throw new IllegalStateException ("Unexpected value: " + state );
332
+ };
333
+
334
+ if (!successful ) {
335
+ return false ;
336
+ }
337
+
338
+ pos += 1 ;
339
+ }
340
+
341
+ return checkParserState ();
342
+ }
343
+
344
+ private boolean checkParserState () {
345
+ if (state != FieldParseState .FINISHED ) {
346
+ parent .reportIssue (SYNTAX_ERROR_MESSAGE );
347
+ return false ;
348
+ }
349
+ return true ;
350
+ }
351
+
352
+ private boolean tryParseFormatSpecifier (char current ) {
311
353
if (current == '{' ) {
312
- nesting ++;
313
- state = ParseState .FORMAT_LCURLY ;
354
+ if (!tryParsingNestedField ()) {
355
+ return false ;
356
+ }
314
357
} else if (current == '}' ) {
315
- result .add (createField (currentFieldName ));
316
- nesting --;
317
- state = ParseState .INIT ;
358
+ addCurrentField ();
359
+ state = FieldParseState .FINISHED ;
318
360
}
361
+ return true ;
362
+ }
363
+
364
+ private boolean tryParsingNestedField () {
365
+ if (this .nesting > 0 ) {
366
+ parent .reportIssue ("Fix this formatted string's syntax; Deep nesting is not allowed." );
367
+ return false ;
368
+ }
369
+ FieldParser fieldParser = new FieldParser (parent , value .substring (pos ), this .nesting + 1 );
370
+
371
+ boolean successful = fieldParser .tryParse ();
372
+ this .pos += fieldParser .getPos () - 1 ;
373
+ return successful ;
319
374
}
320
375
321
376
private boolean tryParseFlagCharacter (char current ) {
322
377
if (current == ':' ) {
323
- state = ParseState .FORMAT ;
378
+ state = FieldParseState .FORMAT ;
324
379
} else if (current == '}' ) {
325
- result .add (createField (currentFieldName ));
326
- nesting --;
327
- state = ParseState .INIT ;
380
+ addCurrentField ();
381
+ state = FieldParseState .FINISHED ;
328
382
} else {
329
- issueReporter . accept (SYNTAX_ERROR_MESSAGE );
383
+ parent . reportIssue (SYNTAX_ERROR_MESSAGE );
330
384
return false ;
331
385
}
332
386
@@ -335,26 +389,33 @@ private boolean tryParseFlagCharacter(char current) {
335
389
336
390
private boolean tryParseField (char current ) {
337
391
if (current == '!' ) {
338
- state = ParseState .FLAG ;
392
+ state = FieldParseState .FLAG ;
339
393
} else if (current == ':' ) {
340
- state = ParseState .FORMAT ;
394
+ state = FieldParseState .FORMAT ;
341
395
} else if (current == '}' ) {
342
- nesting --;
343
- result .add (createField (currentFieldName ));
344
- state = ParseState .INIT ;
396
+ addCurrentField ();
397
+ state = FieldParseState .FINISHED ;
345
398
} else {
346
- issueReporter . accept (SYNTAX_ERROR_MESSAGE );
399
+ parent . reportIssue (SYNTAX_ERROR_MESSAGE );
347
400
return false ;
348
401
}
349
402
return true ;
350
403
}
351
404
405
+ private boolean tryParseFlag (char current ) {
406
+ if (FORMAT_VALID_CONVERSION_FLAGS .indexOf (current ) == -1 ) {
407
+ parent .reportIssue (String .format ("Fix this formatted string's syntax; !%c is not a valid conversion flag." , current ));
408
+ return false ;
409
+ }
410
+ state = FieldParseState .FLAG_CHARACTER ;
411
+ return true ;
412
+ }
413
+
352
414
private int parseFieldName (char current , int pos ) {
353
415
if (current == '{' ) {
354
- state = ParseState . INIT ;
416
+ state = FieldParseState . FINISHED ;
355
417
} else {
356
- state = ParseState .FIELD ;
357
- nesting ++;
418
+ state = FieldParseState .FIELD ;
358
419
if (fieldContentMatcher .region (pos , value .length ()).find ()) {
359
420
// This should always match (if nothing else, an empty string), but be defensive
360
421
currentFieldName = fieldContentMatcher .group ("name" );
@@ -365,21 +426,12 @@ private int parseFieldName(char current, int pos) {
365
426
return pos ;
366
427
}
367
428
368
- private ReplacementField createField (@ Nullable String name ) {
369
- if (name == null ) {
370
- hasAutoNumbering = true ;
371
- int currentPos = autoNumberingPos ;
372
- autoNumberingPos ++;
373
- return new PositionalField (DO_NOTHING_VALIDATOR , currentPos );
374
- } else if (FORMAT_NUMBER_PATTERN .matcher (name ).find ()) {
375
- hasManualNumbering = true ;
376
- return new PositionalField (DO_NOTHING_VALIDATOR , Integer .parseInt (name ));
377
- } else {
378
- return new NamedField (DO_NOTHING_VALIDATOR , name );
379
- }
429
+ private void addCurrentField () {
430
+ parent .addField (currentFieldName );
380
431
}
381
432
}
382
433
434
+
383
435
public static Optional <StringFormat > createFromStrFormatStyle (Consumer <String > issueReporter , String value ) {
384
436
// Format -> '{' [FieldName] ['!' Conversion] [':' FormatSpec*] '}'
385
437
// FormatSpec -> '{' [FieldName] '}' | Character
0 commit comments