ParenthesesTreeParser.java
001 /*
002  * Java Genetic Algorithm Library (jenetics-7.1.2).
003  * Copyright (c) 2007-2023 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 switch (c) {
085             case '('')'',' -> true;
086             default -> false;
087         };
088     }
089 
090     /**
091      * Parses the given parentheses' tree string
092      *
093      @since 4.3
094      *
095      @param <B> the tree node value type
096      @param value the parentheses tree string
097      @param mapper the mapper which converts the serialized string value to
098      *        the desired type
099      @return the parsed tree object
100      @throws NullPointerException if one of the arguments is {@code null}
101      @throws IllegalArgumentException if the given parentheses tree string
102      *         doesn't represent a valid tree
103      */
104     static <B> TreeNode<B> parse(
105         final String value,
106         final Function<? super String, ? extends B> mapper
107     ) {
108         requireNonNull(value);
109         requireNonNull(mapper);
110 
111         final TreeNode<B> root = TreeNode.of();
112         final Deque<TreeNode<B>> parents = new ArrayDeque<>();
113 
114         TreeNode<B> current = root;
115         for (Token token : tokenize(value.trim())) {
116             switch (token.seq) {
117                 case "(" -> {
118                     if (current == null) {
119                         throw new IllegalArgumentException(format(
120                             "Illegal parentheses tree string: '%s'.",
121                             value
122                         ));
123                     }
124                     final TreeNode<B> tn1 = TreeNode.of();
125                     current.attach(tn1);
126                     parents.push(current);
127                     current = tn1;
128                 }
129                 case "," -> {
130                     if (parents.isEmpty()) {
131                         throw new IllegalArgumentException(format(
132                             "Expect '(' at position %d.",
133                             token.pos
134                         ));
135                     }
136                     final TreeNode<B> tn2 = TreeNode.of();
137                     assert parents.peek() != null;
138                     parents.peek().attach(tn2);
139                     current = tn2;
140                 }
141                 case ")" -> {
142                     if (parents.isEmpty()) {
143                         throw new IllegalArgumentException(format(
144                             "Unbalanced parentheses at position %d.",
145                             token.pos
146                         ));
147                     }
148                     current = parents.pop();
149                     if (parents.isEmpty()) {
150                         current = null;
151                     }
152                 }
153                 default -> {
154                     if (current == null) {
155                         throw new IllegalArgumentException(format(
156                             "More than one root element at pos %d: '%s'.",
157                             token.pos, value
158                         ));
159                     }
160                     if (current.value() == null) {
161                         current.value(mapper.apply(unescape(token.seq)));
162                     }
163                 }
164             }
165         }
166 
167         if (!parents.isEmpty()) {
168             throw new IllegalArgumentException(
169                 "Unbalanced parentheses: " + value
170             );
171         }
172 
173         return root;
174     }
175 
176 }