ParenthesesTreeParser.java
001 /*
002  * Java Genetic Algorithm Library (jenetics-7.1.1).
003  * Copyright (c) 2007-2022 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 6.0
038  @since 4.3
039  */
040 final class ParenthesesTreeParser {
041     private ParenthesesTreeParser() {}
042 
043     /**
044      * Represents a parentheses tree string token.
045      */
046     record Token(String seq, int pos) {}
047 
048     /**
049      * Tokenize the given parentheses string.
050      *
051      @param value the parentheses string
052      @return the parentheses string tokens
053      @throws NullPointerException if the given {@code value} is {@code null}
054      */
055     static List<Token> tokenize(final String value) {
056         final List<Token> tokens = new ArrayList<>();
057 
058         char pc = '\0';
059         int pos = 0;
060         final StringBuilder token = new StringBuilder();
061         for (int i = 0; i < value.length(); ++i) {
062             final char c = value.charAt(i);
063 
064             if (isTokenSeparator(c&& pc != ESCAPE_CHAR) {
065                 tokens.add(new Token(token.toString(), pos));
066                 tokens.add(new Token(Character.toString(c), i));
067                 token.setLength(0);
068                 pos = i;
069             else {
070                 token.append(c);
071             }
072 
073             pc = c;
074         }
075 
076         if (!token.isEmpty()) {
077             tokens.add(new Token(token.toString(), pos));
078         }
079 
080         return tokens;
081     }
082 
083     private static boolean isTokenSeparator(final char c) {
084         return c == '(' || c == ')' || c == ',';
085     }
086 
087     /**
088      * Parses the given parentheses tree string
089      *
090      @since 4.3
091      *
092      @param <B> the tree node value type
093      @param value the parentheses tree string
094      @param mapper the mapper which converts the serialized string value to
095      *        the desired type
096      @return the parsed tree object
097      @throws NullPointerException if one of the arguments is {@code null}
098      @throws IllegalArgumentException if the given parentheses tree string
099      *         doesn't represent a valid tree
100      */
101     static <B> TreeNode<B> parse(
102         final String value,
103         final Function<? super String, ? extends B> mapper
104     ) {
105         requireNonNull(value);
106         requireNonNull(mapper);
107 
108         final TreeNode<B> root = TreeNode.of();
109         final Deque<TreeNode<B>> parents = new ArrayDeque<>();
110 
111         TreeNode<B> current = root;
112         for (Token token : tokenize(value.trim())) {
113             switch (token.seq) {
114                 case "(" -> {
115                     if (current == null) {
116                         throw new IllegalArgumentException(format(
117                             "Illegal parentheses tree string: '%s'.",
118                             value
119                         ));
120                     }
121                     final TreeNode<B> tn1 = TreeNode.of();
122                     current.attach(tn1);
123                     parents.push(current);
124                     current = tn1;
125                 }
126                 case "," -> {
127                     if (parents.isEmpty()) {
128                         throw new IllegalArgumentException(format(
129                             "Expect '(' at position %d.",
130                             token.pos
131                         ));
132                     }
133                     final TreeNode<B> tn2 = TreeNode.of();
134                     assert parents.peek() != null;
135                     parents.peek().attach(tn2);
136                     current = tn2;
137                 }
138                 case ")" -> {
139                     if (parents.isEmpty()) {
140                         throw new IllegalArgumentException(format(
141                             "Unbalanced parentheses at position %d.",
142                             token.pos
143                         ));
144                     }
145                     current = parents.pop();
146                     if (parents.isEmpty()) {
147                         current = null;
148                     }
149                 }
150                 default -> {
151                     if (current == null) {
152                         throw new IllegalArgumentException(format(
153                             "More than one root element at pos %d: '%s'.",
154                             token.pos, value
155                         ));
156                     }
157                     if (current.value() == null) {
158                         current.value(mapper.apply(unescape(token.seq)));
159                     }
160                 }
161             }
162         }
163 
164         if (!parents.isEmpty()) {
165             throw new IllegalArgumentException(
166                 "Unbalanced parentheses: " + value
167             );
168         }
169 
170         return root;
171     }
172 
173 }