package com.github.fge.grappa.misc;
import com.github.fge.grappa.annotations.Cached;
import com.github.fge.grappa.annotations.DontLabel;
import com.github.fge.grappa.matchers.EmptyMatcher;
import com.github.fge.grappa.matchers.delegate.OptionalMatcher;
import com.github.fge.grappa.matchers.join.JoinMatcherBuilder;
import com.github.fge.grappa.matchers.repeat.RepeatMatcherBuilder;
import com.github.fge.grappa.parsers.BaseParser;
import com.github.fge.grappa.rules.Rule;
import com.google.common.base.Preconditions;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.Range;
import java.util.Objects;
/**
* A matcher builder for matchers repeating a given number of times
*
* <p>This class allows to specify a number of times which a given rule should
* be repeated:</p>
*
* <ul>
* <li>at least n times;</li>
* <li>at most n times;</li>
* <li>exactly n times;</li>
* <li>between n1 and n2 times.</li>
* </ul>
*
* <p>When appropriate, the returned rule is a simplified version; for instance,
* a rule repeated "exactly once" is the rule itself. See {@link #range(Range)}
* for more details.</p>
*
* @param <V> the type parameter of the parser
*
* @see JoinMatcherBuilder
* @see RepeatMatcherBuilder
*/
public abstract class RangeMatcherBuilder<V>
{
private static final Range<Integer> AT_LEAST_ZERO = Range.atLeast(0);
protected final BaseParser<V> parser;
protected final Rule rule;
protected RangeMatcherBuilder(final BaseParser<V> parser, final Rule rule)
{
this.parser = Objects.requireNonNull(parser);
this.rule = Objects.requireNonNull(rule);
}
/**
* Return a rule with a minimum number of cycles to run
*
* @param nrCycles the number of cycles
* @return a rule
* @throws IllegalArgumentException {@code nrCycles} is less than 0
*
* @see Range#atLeast(Comparable)
*/
public Rule min(final int nrCycles)
{
Preconditions.checkArgument(nrCycles >= 0,
"illegal repetition number specified (" + nrCycles
+ "), must be 0 or greater");
return range(Range.atLeast(nrCycles));
}
/**
* Return a rule with a maximum number of cycles to run
*
* @param nrCycles the number of cycles
* @return a rule
* @throws IllegalArgumentException {@code nrCycles} is less than 0
*
* @see Range#atMost(Comparable)
*/
public Rule max(final int nrCycles)
{
Preconditions.checkArgument(nrCycles >= 0,
"illegal repetition number specified (" + nrCycles
+ "), must be 0 or greater");
return range(Range.atMost(nrCycles));
}
/**
* Return a rule with an exact number of cycles to run
*
* @param nrCycles the number of cycles
* @return a rule
* @throws IllegalArgumentException {@code nrCycles} is less than 0
*
* @see Range#singleton(Comparable)
*/
public Rule times(final int nrCycles)
{
Preconditions.checkArgument(nrCycles >= 0,
"illegal repetition number specified (" + nrCycles
+ "), must be 0 or greater");
return range(Range.singleton(nrCycles));
}
/**
* Return a rule with both lower and upper bounds on the number of cycles
*
* <p>Note that the range of cycles to run is closed on both ends (that is,
* the minimum and maximum number of cycles) are <strong>inclusive</strong>.
* </p>
*
* @param minCycles the minimum number of cycles
* @param maxCycles the maximum number of cycles
* @return a rule
* @throws IllegalArgumentException minimum number of cycles is negative; or
* maximum number of cycles is less than the minimum
*
* @see Range#closed(Comparable, Comparable)
*/
public Rule times(final int minCycles, final int maxCycles)
{
Preconditions.checkArgument(minCycles >= 0,
"illegal repetition number specified (" + minCycles
+ "), must be 0 or greater");
Preconditions.checkArgument(maxCycles >= minCycles,
"illegal range specified (" + minCycles + ", " + maxCycles
+ "): maximum must be greater than minimum");
return range(Range.closed(minCycles, maxCycles));
}
/**
* Core method for building a repeating matcher
*
* <p>This is the method which all other methods (min, max, times) delegate
* to; among other things it is responsible for the logic of simplifying
* matchers where possible.</p>
*
* <p>The simplifications are as follows:</p>
*
* <ul>
* <li>[0..0]: returns an {@link EmptyMatcher};</li>
* <li>[0..1]: returns an {@link OptionalMatcher} with the rule as a
* submatcher;</li>
* <li>[1..1]: returns the rule itself.</li>
* </ul>
*
* <p>If none of these apply, this method delegates as follows:</p>
*
* <ul>
* <li>[n..+∞) for whatever n: delegates to {@link #boundedDown(int)};
* </li>
* <li>[0..n] for n >= 2: delegates to {@link #boundedUp(int)};</li>
* <li>[n..n] for n >= 2: delegates to {@link #exactly(int)};</li>
* <li>[n..m] with 0 < n < m: delegates to {@link
* #boundedBoth(int, int)}.</li>
* </ul>
*
* @param range the range
* @return the final resulting rule
*/
@Cached
@DontLabel
public Rule range(final Range<Integer> range)
{
Objects.requireNonNull(range, "range must not be null");
final Range<Integer> realRange = normalizeRange(range);
/*
* We always have a lower bound
*/
final int lowerBound = realRange.lowerEndpoint();
/*
* Handle the case where there is no upper bound
*/
if (!realRange.hasUpperBound())
return boundedDown(lowerBound);
/*
* There is an upper bound. Handle the case where it is 0 or 1. Since
* the range is legal, we know that if it is 0, so is the lowerbound;
* and if it is one, the lower bound is either 0 or 1.
*/
final int upperBound = realRange.upperEndpoint();
if (upperBound == 0)
return parser.empty();
if (upperBound == 1)
return lowerBound == 0 ? parser.optional(rule) : rule;
/*
* So, upper bound is 2 or greater; return the appropriate matcher
* according to what the lower bound is.
*
* Also, if the lower and upper bounds are equal, return a matcher doing
* a fixed number of matches.
*/
if (lowerBound == 0)
return boundedUp(upperBound);
return lowerBound == upperBound
? exactly(lowerBound)
: boundedBoth(lowerBound, upperBound);
}
/**
* Build a rule which is expected to match a minimum number of times
*
* <p>The returned matcher will attempt to match indefinitely its input
* until it fails. Success should be declared if and only if the number of
* times the matcher has succeeded is greater than, or equal to, the number
* of cycles given as an argument (including 0).</p>
*
* @param minCycles the minimum number of cycles (inclusive)
* @return a rule
*/
protected abstract Rule boundedDown(int minCycles);
/**
* Build a rule which is expected to match a maximum number of times
*
* <p>The returned matcher will attempt to match repeatedly up to, and
* including, the number of cycles returned as an argument. Note that the
* argument will always be greater than or equal to 2.</p>
*
* <p>One consequence is that this matcher always succeeds.</p>
*
* @param maxCycles the maximum number of cycles (inclusive)
* @return a rule
*/
protected abstract Rule boundedUp(int maxCycles);
/**
* Build a rule which is expected to match a fixed number of times
*
* <p>The returned matcher will attempt to match repeatedly up to, and
* including, the number of cycles given as an argument. Success should be
* declared if and only if the number of cycles matched is exactly this
* number.</p>
*
* @param nrCycles the number of cycles (inclusive)
* @return a rule
*/
protected abstract Rule exactly(int nrCycles);
/**
* Build a rule which is expected to match a number of times between two
* end points
*
* <p>The returned matcher will attempt to match repeatedly up to, and
* including, the maximum number of cycles specified as the second argument.
* Success should be declared if and only if the number of cycles performed
* is at least equal to the number of cycles specified as the first
* argument.</p>
*
* <p>Note that the first argument will always be strictly greater than 0,
* and that the second argument will always be strictly greater than the
* first.</p>
*
* @param minCycles the minimum number of cycles (inclusive)
* @param maxCycles the maximum number of cycles (exclusive)
* @return a rule
*/
protected abstract Rule boundedBoth(int minCycles, int maxCycles);
private static Range<Integer> normalizeRange(final Range<Integer> range)
{
Range<Integer> newRange = AT_LEAST_ZERO.intersection(range);
if (newRange.isEmpty())
throw new IllegalArgumentException("illegal range " + range
+ ": intersection with " + AT_LEAST_ZERO + " is empty");
newRange = newRange.canonical(DiscreteDomain.integers());
final int lowerBound = newRange.lowerEndpoint();
return newRange.hasUpperBound()
? Range.closed(lowerBound, newRange.upperEndpoint() - 1)
: Range.atLeast(lowerBound);
}
}