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 }
|