All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.jenetics.ext.util.ParenthesesTreeParser Maven / Gradle / Ivy

The newest version!
/*
 * Java Genetic Algorithm Library (jenetics-8.1.0).
 * Copyright (c) 2007-2024 Franz Wilhelmstötter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Author:
 *    Franz Wilhelmstötter ([email protected])
 */
package io.jenetics.ext.util;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static io.jenetics.ext.util.ParenthesesTrees.ESCAPE_CHAR;
import static io.jenetics.ext.util.ParenthesesTrees.unescape;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.function.Function;

/**
 * Parses a parentheses string into a {@code TreeNode} object.
 *
 * @author Franz Wilhelmstötter
 * @version 6.0
 * @since 4.3
 */
final class ParenthesesTreeParser {
	private ParenthesesTreeParser() {}

	/**
	 * Represents a parentheses tree string token.
	 */
	record Token(String seq, int pos) {}

	/**
	 * Tokenize the given parentheses string.
	 *
	 * @param value the parentheses string
	 * @return the parentheses string tokens
	 * @throws NullPointerException if the given {@code value} is {@code null}
	 */
	static List tokenize(final String value) {
		final List tokens = new ArrayList<>();

		char pc = '\0';
		int pos = 0;
		final StringBuilder token = new StringBuilder();
		for (int i = 0; i < value.length(); ++i) {
			final char c = value.charAt(i);

			if (isTokenSeparator(c) && pc != ESCAPE_CHAR) {
				tokens.add(new Token(token.toString(), pos));
				tokens.add(new Token(Character.toString(c), i));
				token.setLength(0);
				pos = i;
			} else {
				token.append(c);
			}

			pc = c;
		}

		if (!token.isEmpty()) {
			tokens.add(new Token(token.toString(), pos));
		}

		return tokens;
	}

	private static boolean isTokenSeparator(final char c) {
		return switch (c) {
			case '(', ')', ',' -> true;
			default -> false;
		};
	}

	/**
	 * Parses the given parentheses' tree string
	 *
	 * @since 4.3
	 *
	 * @param  the tree node value type
	 * @param value the parentheses tree string
	 * @param mapper the mapper which converts the serialized string value to
	 *        the desired type
	 * @return the parsed tree object
	 * @throws NullPointerException if one of the arguments is {@code null}
	 * @throws IllegalArgumentException if the given parentheses tree string
	 *         doesn't represent a valid tree
	 */
	static  TreeNode parse(
		final String value,
		final Function mapper
	) {
		requireNonNull(value);
		requireNonNull(mapper);

		final TreeNode root = TreeNode.of();
		final Deque> parents = new ArrayDeque<>();

		TreeNode current = root;
		for (Token token : tokenize(value.trim())) {
			switch (token.seq) {
				case "(" -> {
					if (current == null) {
						throw new IllegalArgumentException(format(
							"Illegal parentheses tree string: '%s'.",
							value
						));
					}
					final TreeNode tn1 = TreeNode.of();
					current.attach(tn1);
					parents.push(current);
					current = tn1;
				}
				case "," -> {
					if (parents.isEmpty()) {
						throw new IllegalArgumentException(format(
							"Expect '(' at position %d.",
							token.pos
						));
					}
					final TreeNode tn2 = TreeNode.of();
					assert parents.peek() != null;
					parents.peek().attach(tn2);
					current = tn2;
				}
				case ")" -> {
					if (parents.isEmpty()) {
						throw new IllegalArgumentException(format(
							"Unbalanced parentheses at position %d.",
							token.pos
						));
					}
					current = parents.pop();
					if (parents.isEmpty()) {
						current = null;
					}
				}
				default -> {
					if (current == null) {
						throw new IllegalArgumentException(format(
							"More than one root element at pos %d: '%s'.",
							token.pos, value
						));
					}
					if (current.value() == null) {
						current.value(mapper.apply(unescape(token.seq)));
					}
				}
			}
		}

		if (!parents.isEmpty()) {
			throw new IllegalArgumentException(
				"Unbalanced parentheses: " + value
			);
		}

		return root;
	}

}