/*
* Copyright (C) 2014 Francis Galiegue <fgaliegue@gmail.com>
*
* 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.
*/
package com.github.fge.grappa.matchers.trie;
import javax.annotation.concurrent.Immutable;
import java.nio.CharBuffer;
import java.util.Arrays;
/**
* The core of the trie
*
* <p>This class implements a trie node. It has two elements in it:</p>
*
* <ul>
* <li>a boolean telling whether this node matches a full word;</li>
* <li>a list of children trie nodes, if any.</li>
* </ul>
*
* <p>The children are indexed by the character they match (which means, in
* effect, that a trie node has no characters "belonging" to him, and the root
* node knows of all first characters there are to match).</p>
*
* @since 1.0.0-beta.6
*/
@Immutable
public final class TrieNode
{
private final boolean fullWord;
private final char[] nextChars;
private final TrieNode[] nextNodes;
@SuppressWarnings("MethodCanBeVariableArityMethod")
TrieNode(final boolean fullWord, final char[] nextChars,
final TrieNode[] nextNodes)
{
this.fullWord = fullWord;
this.nextChars = nextChars;
this.nextNodes = nextNodes;
}
public int search(final String needle, final boolean ignoreCase)
{
return doSearch(CharBuffer.wrap(needle), fullWord ? 0 : -1, 0,
ignoreCase);
}
/**
* Core search method
*
* <p>This method uses a {@link CharBuffer} to perform searches, and changes
* this buffer's position as the match progresses. The two other arguments
* are the depth of the current search (ie the number of nodes visited
* since root) and the index of the last node where a match was found (ie
* the last node where {@link #fullWord} was true.</p>
*
* @param buffer the charbuffer
* @param matchedLength the last matched length (-1 if no match yet)
* @param currentLength the current length walked by the trie
* @return the length of the match found, -1 otherwise
*/
private int doSearch(final CharBuffer buffer, final int matchedLength,
final int currentLength, final boolean ignoreCase)
{
/*
* Try and see if there is a possible match here; there is if "fullword"
* is true, in this case the next "matchedLength" argument to a possible
* child call will be the current length.
*/
final int nextLength = fullWord ? currentLength : matchedLength;
/*
* If there is nothing left in the buffer, we have a match.
*/
if (!buffer.hasRemaining())
return nextLength;
/*
* OK, there is at least one character remaining, so pick it up and see
* whether it is in the list of our children...
*/
char c = buffer.get();
int index = Arrays.binarySearch(nextChars, c);
if (index < 0 && ignoreCase) {
final boolean isUpper = Character.isUpperCase(c);
final boolean isLower = Character.isLowerCase(c);
if (isUpper != isLower) {
c = isUpper ? Character.toLowerCase(c)
: Character.toUpperCase(c);
index = Arrays.binarySearch(nextChars, c);
}
}
/*
* If not, we return the last good match; if yes, we call this same
* method on the matching child node with the (possibly new) matched
* length as an argument and a depth increased by 1.
*/
if (index < 0)
return nextLength;
return nextNodes[index].doSearch(buffer, nextLength, currentLength + 1,
ignoreCase);
}
}