TreeParser.java
001 /*
002  * Java Genetic Algorithm Library (jenetics-5.2.0).
003  * Copyright (c) 2007-2020 Franz Wilhelmstötter
004  *
005  * Licensed under the Apache License, Version 2.0 (the "License");
006  * you may not use this file except in compliance with the License.
007  * You may obtain a copy of the License at
008  *
009  *      http://www.apache.org/licenses/LICENSE-2.0
010  *
011  * Unless required by applicable law or agreed to in writing, software
012  * distributed under the License is distributed on an "AS IS" BASIS,
013  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014  * See the License for the specific language governing permissions and
015  * limitations under the License.
016  *
017  * Author:
018  *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019  */
020 package io.jenetics.ext.util;
021 
022 import static java.lang.String.format;
023 import static java.util.Objects.requireNonNull;
024 import static io.jenetics.ext.util.ParenthesesTrees.ESCAPE_CHAR;
025 import static io.jenetics.ext.util.ParenthesesTrees.unescape;
026 
027 import java.util.ArrayDeque;
028 import java.util.ArrayList;
029 import java.util.Deque;
030 import java.util.List;
031 import java.util.function.Function;
032 
033 /**
034  * Parses an parentheses string into a {@code TreeNode<String>} object.
035  *
036  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
037  @version 4.3
038  @since 4.3
039  */
040 final class TreeParser {
041     private TreeParser() {}
042 
043     /**
044      * Represents a parentheses tree string token.
045      */
046     final static class Token {
047         final String seq;
048         final int pos;
049 
050         Token(final String seq, final int pos) {
051             this.seq = seq;
052             this.pos = pos;
053         }
054     }
055 
056     /**
057      * Tokenize the given parentheses string.
058      *
059      @param value the parentheses string
060      @return the parentheses string tokens
061      @throws NullPointerException if the given {@code value} is {@code null}
062      */
063     static List<Token> tokenize(final String value) {
064         final List<Token> tokens = new ArrayList<>();
065 
066         char pc = '\0';
067         int pos = 0;
068         final StringBuilder token = new StringBuilder();
069         for (int i = 0; i < value.length(); ++i) {
070             final char c = value.charAt(i);
071 
072             if (isTokenSeparator(c&& pc != ESCAPE_CHAR) {
073                 tokens.add(new Token(unescape(token.toString()), pos));
074                 tokens.add(new Token(Character.toString(c), i));
075                 token.setLength(0);
076                 pos = i;
077             else {
078                 token.append(c);
079             }
080 
081             pc = c;
082         }
083 
084         if (token.length() 0) {
085             tokens.add(new Token(unescape(token.toString()), pos));
086         }
087 
088         return tokens;
089     }
090 
091     private static boolean isTokenSeparator(final char c) {
092         return c == '(' || c == ')' || c == ',';
093     }
094 
095     /**
096      * Parses the given parentheses tree string
097      *
098      @since 4.3
099      *
100      @param <B> the tree node value type
101      @param value the parentheses tree string
102      @param mapper the mapper which converts the serialized string value to
103      *        the desired type
104      @return the parsed tree object
105      @throws NullPointerException if one of the arguments is {@code null}
106      @throws IllegalArgumentException if the given parentheses tree string
107      *         doesn't represent a valid tree
108      */
109     static <B> TreeNode<B> parse(
110         final String value,
111         final Function<? super String, ? extends B> mapper
112     ) {
113         requireNonNull(value);
114         requireNonNull(mapper);
115 
116         final TreeNode<B> root = TreeNode.of();
117         final Deque<TreeNode<B>> parents = new ArrayDeque<>();
118 
119         TreeNode<B> current = root;
120         for (Token token : tokenize(value.trim())) {
121             switch (token.seq) {
122                 case "(":
123                     if (current == null) {
124                         throw new IllegalArgumentException(format(
125                             "Illegal parentheses tree string: '%s'.",
126                             value
127                         ));
128                     }
129 
130                     final TreeNode<B> tn1 = TreeNode.of();
131                     current.attach(tn1);
132                     parents.push(current);
133                     current = tn1;
134                     break;
135                 case ",":
136                     if (parents.isEmpty()) {
137                         throw new IllegalArgumentException(format(
138                             "Expect '(' at position %d.",
139                             token.pos
140                         ));
141                     }
142 
143                     final TreeNode<B> tn2 = TreeNode.of();
144                     assert parents.peek() != null;
145                     parents.peek().attach(tn2);
146                     current = tn2;
147                     break;
148                 case ")":
149                     if (parents.isEmpty()) {
150                         throw new IllegalArgumentException(format(
151                             "Unbalanced parentheses at position %d.",
152                             token.pos
153                         ));
154                     }
155                     current = parents.pop();
156                     if (parents.isEmpty()) {
157                         current = null;
158                     }
159                     break;
160                 default:
161                     if (current == null) {
162                         throw new IllegalArgumentException(format(
163                             "More than one root element at pos %d: '%s'.",
164                             token.pos, value
165                         ));
166                     }
167                     if (current.value() == null) {
168                         current.value(mapper.apply(token.seq));
169                     }
170                     break;
171             }
172         }
173 
174         if (!parents.isEmpty()) {
175             throw new IllegalArgumentException(
176                 "Unbalanced parentheses: " + value
177             );
178         }
179 
180         return root;
181     }
182 
183 }