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