diff options
| author | Benjamin J. Culkin <bjculkin@mix.wvu.edu> | 2017-10-08 22:39:59 -0300 |
|---|---|---|
| committer | Benjamin J. Culkin <bjculkin@mix.wvu.edu> | 2017-10-08 22:39:59 -0300 |
| commit | c82e3b3b2de0633317ec8fc85925e91422820597 (patch) | |
| tree | 96567416ce23c5ce85601f9cedc3a94bb1c55cba /base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java | |
| parent | b3ac1c8690c3e14c879913e5dcc03a5f5e14876e (diff) | |
Start splitting into maven modules
Diffstat (limited to 'base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java')
| -rw-r--r-- | base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java b/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java new file mode 100644 index 0000000..ccfaffb --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java @@ -0,0 +1,371 @@ +package bjc.utils.parserutils.delims; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; + +import bjc.utils.data.IPair; +import bjc.utils.data.ITree; +import bjc.utils.data.Tree; +import bjc.utils.esodata.PushdownMap; +import bjc.utils.esodata.SimpleStack; +import bjc.utils.esodata.Stack; +import bjc.utils.funcdata.IMap; +import bjc.utils.funcutils.StringUtils; + +/** + * Convert linear sequences into trees that represent group structure. + * + * @author EVE + * + * @param <T> + * The type of items in the sequence. + */ +public class SequenceDelimiter<T> { + /* + * Mapping from group names to actual groups. + */ + private final Map<T, DelimiterGroup<T>> groups; + + /* + * The initial group to start with. + */ + private DelimiterGroup<T> initialGroup; + + /** + * Create a new sequence delimiter. + */ + public SequenceDelimiter() { + groups = new HashMap<>(); + } + + /** + * Convert a linear sequence into a tree that matches the delimiter + * structure. + * + * Essentially, creates a parse tree of the expression against the + * following grammar while obeying the defined grouping rules. + * + * <pre> + * <tree> → (<data> | <subgroup> | <group>)* + * <subgroup> → <tree> <marker> + * <group> → <open> <tree> <close> + * + * <data> → STRING + * <open> → STRING + * <close> → STRING + * <marker> → STRING + * </pre> + * + * @param chars + * The parameters on how to mark certain portions of the + * tree. + * @param seq + * The sequence to delimit. + * + * @return The sequence as a tree that matches its group structure. Each + * node in the tree is either a data node, a subgroup node, or a + * group node. + * + * A data node is a leaf node whose data is the string it + * represents. + * + * A subgroup node is a node with two children, and the name of + * the sub-group as its label. The first child is the contents + * of the sub-group, and the second is the marker that started + * the subgroup. The marker is a leaf node labeled with its + * contents, and the contents contains a recursive tree. + * + * A group node is a node with three children, and the name of + * the group as its label. The first child is the opening + * delimiter, the second is the group contents, and the third is + * the closing delimiter. The delimiters are leaf nodes labeled + * with their contents, while the group node contains a + * recursive tree. + * + * @throws DelimiterException + * Thrown if something went wrong during sequence + * delimitation. + * + */ + public ITree<T> delimitSequence(final SequenceCharacteristics<T> chars, + @SuppressWarnings("unchecked") final T... seq) throws DelimiterException { + if (initialGroup == null) throw new NullPointerException("Initial group must be specified."); + else if (chars == null) throw new NullPointerException("Sequence characteristics must not be null"); + + /* + * The stack of opened and not yet closed groups. + */ + final Stack<DelimiterGroup<T>.OpenGroup> groupStack = new SimpleStack<>(); + + /* + * Open initial group. + */ + groupStack.push(initialGroup.open(chars.root, null)); + + /* + * Groups that aren't allowed to be opened at the moment. + */ + final Stack<Multiset<T>> forbiddenDelimiters = new SimpleStack<>(); + forbiddenDelimiters.push(HashMultiset.create()); + + /* + * Groups that are allowed to be opened at the moment. + */ + final Stack<Multimap<T, T>> allowedDelimiters = new SimpleStack<>(); + allowedDelimiters.push(HashMultimap.create()); + + /* + * Map of who forbid what for debugging purposes. + */ + final IMap<T, T> whoForbid = new PushdownMap<>(); + + /* + * Process each member of the sequence. + */ + for (int i = 0; i < seq.length; i++) { + final T tok = seq[i]; + + /* + * Check if this token could open a group. + */ + final IPair<T, T[]> possibleOpenPar = groupStack.top().doesOpen(tok); + T possibleOpen = possibleOpenPar.getLeft(); + + if (possibleOpen == null) { + /* + * Handle nested openers. + * + * Local openers take priority over nested ones + * if they overlap. + */ + if (allowedDelimiters.top().containsKey(tok)) { + possibleOpen = allowedDelimiters.top().get(tok).iterator().next(); + } + } + + /* + * If we have an opening delimiter, handle it. + */ + if (possibleOpen != null) { + final DelimiterGroup<T> group = groups.get(possibleOpen); + + /* + * Error on groups that can't open in this + * context. + * + * This means groups that can't occur at the + * top-level of this group, as well as nested + * exclusions from all enclosing groups. + */ + if (isForbidden(groupStack, forbiddenDelimiters, possibleOpen)) { + T forbiddenBy; + + if (whoForbid.containsKey(tok)) { + forbiddenBy = whoForbid.get(tok); + } else { + forbiddenBy = groupStack.top().getName(); + } + + final String ctxList = StringUtils.toEnglishList(groupStack.toArray(), "then"); + + final String fmt = "Group '%s' can't be opened in this context. (forbidden by '%s')\nContext Stack: %s"; + + throw new DelimiterException(String.format(fmt, group, forbiddenBy, ctxList)); + } + + /* + * Add an open group. + */ + final DelimiterGroup<T>.OpenGroup open = group.open(tok, possibleOpenPar.getRight()); + groupStack.push(open); + + /* + * Handle 'forgetful' groups that reset nesting + */ + if (open.isForgetful()) { + allowedDelimiters.push(HashMultimap.create()); + forbiddenDelimiters.push(HashMultiset.create()); + } + + /* + * Add the nested opens from this group. + */ + final Multimap<T, T> currentAllowed = allowedDelimiters.top(); + for (final Entry<T, T> opener : open.getNestingOpeners().entrySet()) { + currentAllowed.put(opener.getKey(), opener.getValue()); + } + + /* + * Add the nested exclusions from this group + */ + final Multiset<T> currentForbidden = forbiddenDelimiters.top(); + for (final T exclusion : open.getNestingExclusions()) { + currentForbidden.add(exclusion); + + whoForbid.put(exclusion, possibleOpen); + } + } else if (!groupStack.empty() && groupStack.top().isClosing(tok)) { + /* + * Close the group. + */ + final DelimiterGroup<T>.OpenGroup closed = groupStack.pop(); + + groupStack.top().addItem(closed.toTree(tok, chars)); + + /* + * Remove nested exclusions from this group. + */ + final Multiset<T> currentForbidden = forbiddenDelimiters.top(); + for (final T excludedGroup : closed.getNestingExclusions()) { + currentForbidden.remove(excludedGroup); + + whoForbid.remove(excludedGroup); + } + + /* + * Remove the nested opens from this group. + */ + final Multimap<T, T> currentAllowed = allowedDelimiters.top(); + for (final Entry<T, T> closer : closed.getNestingOpeners().entrySet()) { + currentAllowed.remove(closer.getKey(), closer.getValue()); + } + + /* + * Handle 'forgetful' groups that reset nesting. + */ + if (closed.isForgetful()) { + allowedDelimiters.drop(); + forbiddenDelimiters.drop(); + } + } else if (!groupStack.empty() && groupStack.top().marksSubgroup(tok)) { + /* + * Mark a subgroup. + */ + groupStack.top().markSubgroup(tok, chars); + } else { + /* + * Add an item to the group. + */ + groupStack.top().addItem(new Tree<>(tok)); + } + } + + /* + * Error if not all groups were closed. + */ + if (groupStack.size() > 1) { + final DelimiterGroup<T>.OpenGroup group = groupStack.top(); + + final StringBuilder msgBuilder = new StringBuilder(); + + final String closingDelims = StringUtils.toEnglishList(group.getNestingExclusions().toArray(), + false); + + final String ctxList = StringUtils.toEnglishList(groupStack.toArray(), "then"); + + msgBuilder.append("Unclosed group '"); + msgBuilder.append(group.getName()); + msgBuilder.append("'. Expected one of "); + msgBuilder.append(closingDelims); + msgBuilder.append(" to close it\nOpen groups: "); + msgBuilder.append(ctxList); + + final String fmt = "Unclosed group '%s'. Expected one of %s to close it.\nOpen groups: %n"; + + throw new DelimiterException(String.format(fmt, group.getName(), closingDelims, ctxList)); + } + + return groupStack.pop().toTree(chars.root, chars); + } + + /* + * Check if a group is forbidden to open in a context. + */ + private boolean isForbidden(final Stack<DelimiterGroup<T>.OpenGroup> groupStack, + final Stack<Multiset<T>> forbiddenDelimiters, final T groupName) { + boolean localForbid; + + /* + * Check if a delimiter is locally forbidden. + */ + if (groupStack.empty()) { + localForbid = false; + } else { + localForbid = groupStack.top().excludes(groupName); + } + + return localForbid || forbiddenDelimiters.top().contains(groupName); + } + + /** + * Add a delimiter group. + * + * @param group + * The delimiter group. + */ + public void addGroup(final DelimiterGroup<T> group) { + if (group == null) throw new NullPointerException("Group must not be null"); + + groups.put(group.groupName, group); + } + + /** + * Creates and adds a delimiter group using the provided settings. + * + * @param openers + * The tokens that open this group + * @param groupName + * The name of the group + * @param closers + * The tokens that close this group + */ + public void addGroup(final T[] openers, final T groupName, @SuppressWarnings("unchecked") final T... closers) { + final DelimiterGroup<T> group = new DelimiterGroup<>(groupName); + + group.addClosing(closers); + + addGroup(group); + + for (final T open : openers) { + group.addOpener(open, groupName); + } + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + builder.append("SequenceDelimiter ["); + + if (groups != null) { + builder.append("groups="); + builder.append(groups); + builder.append(","); + } + + if (initialGroup != null) { + builder.append("initialGroup="); + builder.append(initialGroup); + } + + builder.append("]"); + + return builder.toString(); + } + + /** + * Set the initial group of this delimiter. + * + * @param initialGroup + * The initial group of this delimiter. + */ + public void setInitialGroup(final DelimiterGroup<T> initialGroup) { + this.initialGroup = initialGroup; + } +} |
