001/*
002 * Java Genetic Algorithm Library (jenetics-8.1.0).
003 * Copyright (c) 2007-2024 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 */
020package io.jenetics.ext.grammar;
021
022import static java.lang.Character.isDigit;
023import static java.lang.Character.isWhitespace;
024import static io.jenetics.ext.internal.parser.CharSequenceTokenizer.isAlphabetic;
025
026import java.util.stream.Collectors;
027
028import io.jenetics.ext.internal.parser.ParsingException;
029
030/**
031 * This class contains methods for parsing and formatting <em>context-free</em>
032 * grammars in
033 * <a href="https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form">BNF</a>
034 * format.
035 * {@snippet lang="java":
036 * final Cfg<String> grammar = Bnf.parse("""
037 *     <expr> ::= <num> | <var> | '(' <expr> <op> <expr> ')'
038 *     <op>   ::= + | - | * | /
039 *     <var>  ::= x | y
040 *     <num>  ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
041 *     """
042 * );
043 * }
044 *
045 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
046 * @since 7.1
047 * @version 7.1
048 */
049public final class Bnf {
050        private Bnf() {}
051
052        static boolean isSymbolChar(final int ch) {
053                return switch (ch) {
054                        case '<', '>', '|', ':', '=' -> true;
055                        default -> false;
056                };
057        }
058
059        static boolean isStringChar(final char c) {
060                return !isWhitespace(c) && !isSymbolChar(c);
061        }
062
063        static boolean isIdChar(final char c) {
064                return isAlphabetic(c) || isDigit(c) || (c == '-');
065        }
066
067        /**
068         * Parses the given BNF {@code grammar} string to a {@link Cfg} object. The
069         * following example shows the grammar of a simple arithmetic expression.
070         *
071         * <pre>{@code
072         * <expr> ::= <num> | <var> | '(' <expr> <op> <expr> ')'
073         * <op>   ::= + | - | * | /
074         * <var>  ::= x | y
075         * <num>  ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
076         * }</pre>
077         *
078         * @param grammar the BNF {@code grammar} string
079         * @return the parsed {@code BNF} object
080         * @throws ParsingException if the given <em>grammar</em> is invalid
081         * @throws NullPointerException it the given {@code grammar} string is
082         *         {@code null}
083         */
084        public static Cfg<String> parse(final String grammar) {
085                final var tokenizer = new BnfTokenizer(grammar);
086                final var parser = new BnfParser(tokenizer);
087
088                return parser.parse();
089        }
090
091        /**
092         * Formats the given <em>CFG</em> as BNF grammar string.
093         *
094         * @param grammar the CFG to format as BNF
095         * @return the BNF formatted grammar string
096         * @throws NullPointerException if the give {@code grammar} is {@code null}
097         */
098        public static String format(final Cfg<?> grammar) {
099                return grammar.rules().stream()
100                        .map(Bnf::format)
101                        .collect(Collectors.joining("\n"));
102        }
103
104        private static String format(final Cfg.Rule<?> rule) {
105                return String.format(
106                        "%s ::= %s",
107                        format(rule.start()),
108                        rule.alternatives().stream()
109                                .map(Bnf::format)
110                                .collect(Collectors.joining("\n    | "))
111                );
112        }
113
114        private static String format(final Cfg.Expression<?> expr) {
115                return expr.symbols().stream()
116                        .map(Bnf::format)
117                        .collect(Collectors.joining(" "));
118        }
119
120        private static String format(final Cfg.Symbol<?> symbol) {
121                if (symbol instanceof Cfg.NonTerminal<?> nt) {
122                        return String.format("<%s>", nt.name());
123                } else if (symbol instanceof Cfg.Terminal<?> t) {
124                        return "'" + t.name()
125                                .replace("\\", "\\\\")
126                                .replace("'", "\\'") + "'";
127                }
128                throw new AssertionError();
129        }
130
131}