@@ -26,6 +26,48 @@ abstract class Keyword {
2626 */
2727 abstract boolean isPresent (String expr );
2828
29+ /**
30+ * Computes the minimum length of input required to match a pattern.
31+ * Walks the linked list of pattern elements and sums mandatory character counts.
32+ * @param first The first element of the pattern.
33+ * @return The minimum number of characters an input must have to possibly match.
34+ */
35+ public static int computeMinLength (PatternElement first ) {
36+ int length = 0 ;
37+ PatternElement next = first ;
38+ while (next != null ) {
39+ switch (next ) {
40+ case LiteralPatternElement ignored -> {
41+ // Only count non-space characters since spaces are somewhat flexible
42+ // underestimation is safe, over is not.
43+ String literal = next .toString ();
44+ for (int i = 0 ; i < literal .length (); i ++) {
45+ if (literal .charAt (i ) != ' ' )
46+ length ++;
47+ }
48+ }
49+ case ChoicePatternElement choicePatternElement -> {
50+ // get min length of options
51+ int min = Integer .MAX_VALUE ;
52+ for (PatternElement choice : choicePatternElement .getPatternElements ()) {
53+ int choiceLen = computeMinLength (choice );
54+ if (choiceLen < min )
55+ min = choiceLen ;
56+ }
57+ if (min != Integer .MAX_VALUE )
58+ length += min ;
59+ }
60+ case GroupPatternElement groupPatternElement ->
61+ length += computeMinLength (groupPatternElement .getPatternElement ());
62+ default -> {
63+ // OptionalPatternElement, TypePatternElement, RegexPatternElement, ParseTagPatternElement: 0 min length
64+ }
65+ }
66+ next = next .originalNext ;
67+ }
68+ return length ;
69+ }
70+
2971 /**
3072 * Builds a list of keywords starting from the provided pattern element.
3173 * @param first The pattern to build keywords from.
@@ -47,24 +89,30 @@ private static Keyword[] buildKeywords(PatternElement first, boolean starting, i
4789 List <Keyword > keywords = new ArrayList <>();
4890 PatternElement next = first ;
4991 while (next != null ) {
50- if (next instanceof LiteralPatternElement ) { // simple literal strings are keywords
51- String literal = next .toString ().trim ();
52- while (literal .contains (" " ))
53- literal = literal .replace (" " , " " );
54- if (!literal .isEmpty ()) // empty string is not useful
55- keywords .add (new SimpleKeyword (literal , starting , next .next == null ));
56- } else if (depth <= 1 && next instanceof ChoicePatternElement ) { // attempt to build keywords from choices
57- final boolean finalStarting = starting ;
58- final int finalDepth = depth ;
59- // build the keywords for each choice
60- Set <Set <Keyword >> choices = ((ChoicePatternElement ) next ).getPatternElements ().stream ()
61- .map (element -> buildKeywords (element , finalStarting , finalDepth ))
62- .map (ImmutableSet ::copyOf )
63- .collect (Collectors .toSet ());
64- if (choices .stream ().noneMatch (Collection ::isEmpty )) // each choice must have a keyword for this to work
65- keywords .add (new ChoiceKeyword (choices )); // a keyword where only one choice much
66- } else if (next instanceof GroupPatternElement ) { // add in keywords from the group
67- Collections .addAll (keywords , buildKeywords (((GroupPatternElement ) next ).getPatternElement (), starting , depth + 1 ));
92+ switch (next ) {
93+ case LiteralPatternElement ignored -> {
94+ String literal = next .toString ().trim ();
95+ while (literal .contains (" " ))
96+ literal = literal .replace (" " , " " );
97+ if (!literal .isEmpty ()) // empty string is not useful
98+ keywords .add (new SimpleKeyword (literal , starting , next .next == null ));
99+ }
100+ case ChoicePatternElement choicePatternElement when depth <= 1 -> {
101+ final boolean finalStarting = starting ;
102+ final int finalDepth = depth ;
103+ // build the keywords for each choice
104+ Set <Set <Keyword >> choices = choicePatternElement .getPatternElements ().stream ()
105+ .map (element -> buildKeywords (element , finalStarting , finalDepth ))
106+ .map (ImmutableSet ::copyOf )
107+ .collect (Collectors .toSet ());
108+ if (choices .stream ().noneMatch (Collection ::isEmpty )) // each choice must have a keyword for this to work
109+ keywords .add (new ChoiceKeyword (choices )); // a keyword where only one choice much
110+ }
111+ case GroupPatternElement groupPatternElement -> // add in keywords from the group
112+ Collections .addAll (keywords , buildKeywords (groupPatternElement .getPatternElement (), starting , depth + 1 ));
113+ default -> {
114+ // OptionalPatternElement, TypePatternElement, RegexPatternElement, ParseTagPatternElement: do not contribute keywords
115+ }
68116 }
69117
70118 // a parse tag does not represent actual content in a pattern, therefore it should not affect starting
@@ -108,12 +156,11 @@ public int hashCode() {
108156 public boolean equals (Object obj ) {
109157 if (this == obj )
110158 return true ;
111- if (!(obj instanceof SimpleKeyword ))
159+ if (!(obj instanceof SimpleKeyword simpleKeyword ))
112160 return false ;
113- SimpleKeyword other = (SimpleKeyword ) obj ;
114- return this .keyword .equals (other .keyword ) &&
115- this .starting == other .starting &&
116- this .ending == other .ending ;
161+ return this .keyword .equals (simpleKeyword .keyword ) &&
162+ this .starting == simpleKeyword .starting &&
163+ this .ending == simpleKeyword .ending ;
117164 }
118165
119166 @ Override
@@ -152,9 +199,9 @@ public int hashCode() {
152199 public boolean equals (Object obj ) {
153200 if (this == obj )
154201 return true ;
155- if (!(obj instanceof ChoiceKeyword ))
202+ if (!(obj instanceof ChoiceKeyword choiceKeyword ))
156203 return false ;
157- return choices .equals ((( ChoiceKeyword ) obj ) .choices );
204+ return choices .equals (choiceKeyword .choices );
158205 }
159206
160207 @ Override
0 commit comments