/*license*\ XBN-Java: Copyright (C) 2014, Jeff Epstein (aliteralmind __DASH__ github __AT__ yahoo __DOT__ com) This software is dual-licensed under the: - Lesser General Public License (LGPL) version 3.0 or, at your option, any later version; - Apache Software License (ASL) version 2.0. Either license may be applied at your discretion. More information may be found at - http://en.wikipedia.org/wiki/Multi-licensing. The text of both licenses is available in the root directory of this project, under the names "LICENSE_lgpl-3.0.txt" and "LICENSE_asl-2.0.txt". The latest copies may be downloaded at: - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt \*license*/ package com.github.xbn.number; import com.github.xbn.lang.Invert; import com.github.xbn.lang.AbstractExtraErrInfoable; import com.github.xbn.lang.CrashIfObject; import com.github.xbn.lang.RuleType; import com.github.xbn.lang.RuleableComposer; import com.github.xbn.lang.Ruleable; import static com.github.xbn.lang.XbnConstants.*; /** * <p>Determines if a number is within a {@code NumberRange}.</p> * @since 0.1.0 * @author Copyright (C) 2014, Jeff Epstein ({@code aliteralmind __DASH__ github __AT__ yahoo __DOT__ com}), dual-licensed under the LGPL (version 3.0 or later) or the ASL (version 2.0). See source code for details. <a href="http://xbnjava.aliteralmind.com">{@code http://xbnjava.aliteralmind.com}</a>, <a href="https://github.com/aliteralmind/xbnjava">{@code https://github.com/aliteralmind/xbnjava}</a> */ public abstract class NumberInRange<N extends Number> extends AbstractExtraErrInfoable implements NumberRange<N>, Ruleable { //state private final NumberBound<N> nbMin ; private final NumberBound<N> nbMax ; private final boolean isInverted; protected final RuleableComposer ruleCmpsr; //constructors...START /** * <p>Create a new {@code NumberInRange}.</p> * * <p>Equal to * <br/>     <code>{@link #NumberInRange(Invert, NumberBound, NumberBound) this}({@link com.github.xbn.lang.Invert}.{@link com.github.xbn.lang.Invert#NO NO}, null, null)</code></p> */ public NumberInRange() { this(Invert.NO, null, null); } /** * <p>Create a new {@code NumberInRange}.</p> * * <p>Equal to * <br/>     <code>{@link #NumberInRange(Invert, NumberBound, NumberBound) this}({@link com.github.xbn.lang.Invert}.{@link com.github.xbn.lang.Invert#NO NO}, nb_min, nb_max)</code></p> */ public NumberInRange(NumberBound<N> nb_min, NumberBound<N> nb_max) { this(Invert.NO, nb_min, nb_max); } /** * <p>Create a new {@code NumberInRange}.</p> * @param nb_min Get with {@link #getMinBound() getMinBound}{@code (null, null)} * @param nb_max Get with {@link #getMaxBound() getMaxBound}{@code ()} * @param invert If {@link com.github.xbn.lang.Invert#YES YES}, then * the range is made opposite--If the bounds are one and three, then * <code><!-- GENERIC PARAMETERS FAIL IN @link --><a href="#isIn(N)">isIn</a>(2)</code> * will return {@code false}. If {@link com.github.xbn.lang.Invert#NO NO}, * the range is treated normally ({@code isIn(2)} returns {@code true}). * May not be {@code null}. Get with {@link #isInverted() isInverted}{@code ()}. * @see #NumberInRange() * @see #NumberInRange(NumberBound, NumberBound) */ public NumberInRange(Invert invert, NumberBound<N> nb_min, NumberBound<N> nb_max) { super(); nbMin = nb_min; nbMax = nb_max; try { isInverted = invert.isYes(); } catch(RuntimeException rx) { throw CrashIfObject.nullOrReturnCause(invert, "invert", null, rx); } crashIfBadBoundsForCnstr(); ruleCmpsr = new RuleableComposer(getRuleTypeFromBounds()); } //setters...START protected RuleType getRuleTypeFromBounds() { return ((getMinBound() == null && getMaxBound() == null) ? (isInverted() ? RuleType.IMPOSSIBLE : RuleType.UNRESTRICTED) : RuleType.RESTRICTED); } //setters...END //getters...START public final RuleType getRuleType() { return ruleCmpsr.getRuleType(); } /** * <p>Is there a minimum bound?.</p> * @return {@code (getMinBound() != null)} * @see #getMinBound() */ public boolean hasMin() { return (getMinBound() != null); } /** * <p>Is there a maximum bound?.</p> * @return <code>({@link #getMaxBound() getMaxBound}() != null)</code> * @see #getMinBound() */ public boolean hasMax() { return (getMaxBound() != null); } /** * <p>The minimum bound.</p> * @see #getMaxBound() * @see #hasMin() * @see #getMinNumber() * @see #isMinInclusive() * @see #NumberInRange(Invert, NumberBound, NumberBound) constructor */ public NumberBound<N> getMinBound() { return nbMin; } /** * <p>The maximum bound.</p> * @see #getMinBound() * @see #hasMax() * @see #getMaxNumber() * @see #isMaxInclusive() * @see #NumberInRange(Invert, NumberBound, NumberBound) constructor */ public NumberBound<N> getMaxBound() { return nbMax; } /** * <p>Is the range inverted?.</p> * @return <ul> * <li>{@code true}: The range is made opposite--If the bounds are * one and three, then * <code><!-- GENERIC PARAMETERS FAIL IN @link --><a href="#isIn(N)">isIn</a>(2)</code> * will return {@code false}</li> * <li>{@code false}: The range is treated normally ({@code isIn(2)} * returns {@code true})</li> * </ul> * @see #NumberInRange(Invert, NumberBound, NumberBound) constructor */ public boolean isInverted() { return isInverted; } /** * <p>Get the minimum bound number.</p> * @return <code>getMinBound().{@link NumberBound#get() get}()</code> * @exception NullPointerException If {@link #hasMin() hasMin}{@code ()} is {@code false}. * @see #getMinBound() */ public N getMinNumber() { try { return getMinBound().get(); } catch(RuntimeException rx) { throwISXIfBoundNull(getMinBound(), "in"); throw rx; } } /** * <p>Is the minimum bound inclusive?.</p> * @return <code>getMinBound().{@link NumberBound#isInclusive() isInclusive}()</code> * @exception NullPointerException If {@code getMinBound()} is {@code null}. * @see #getMinBound() */ public boolean isMinInclusive() { try { return getMinBound().isInclusive(); } catch(RuntimeException rx) { throwISXIfBoundNull(getMinBound(), "in"); throw rx; } } /** * <p>Get the maximum bound number.</p> * @return <code>{@link #getMaxBound() getMaxBound}().{@link NumberBound#get() get}()</code> * @exception NullPointerException If {@code getMaxBound()} is * {@code null}. * @see #getMinBound() */ public N getMaxNumber() { try { return getMaxBound().get(); } catch(RuntimeException rx) { throwISXIfBoundNull(getMaxBound(), "ax"); throw rx; } } /** * <p>Is the maximum bound inclusive?.</p> * @return <code>{@link #getMaxBound() getMaxBound}(){@link NumberBound#isInclusive() isInclusive}()</code> * @exception NullPointerException If {@code getMaxBound()} is {@code null}. * @see #getMinBound() */ public boolean isMaxInclusive() { try { return getMaxBound().isInclusive(); } catch(RuntimeException rx) { throwISXIfBoundNull(getMaxBound(), "ax"); throw rx; } } //getters...END //other...START /** * @return <code>(!{@link #hasMin() hasMin}()  &&  !{@link #hasMax() hasMax}())</code> */ public boolean isUnrestricted() { return (!hasMin() && !hasMax()); } /** * @return {@link #appendRules(StringBuilder) appendRules}{@code ((new StringBuilder())).toString()} */ public String getRules() { return appendRules((new StringBuilder())).toString(); } /** * @see #getRules() * @see <code><a href="http://en.wikibooks.org/wiki/Algebra/Interval_Notation">http://en.wikibooks.org/wiki/Algebra/Interval_Notation</a></code> */ public StringBuilder appendRules(StringBuilder to_appendTo) { NumberBound<?> min = getMinBound(); try { if(isInverted()) { to_appendTo.append("inverted:"); } if(min == null) { to_appendTo.append("[-inf"); } else { to_appendTo.append(min.isInclusive() ? "[" : "(").append(min.get()); } } catch(RuntimeException rx) { throw CrashIfObject.nullOrReturnCause(to_appendTo, "to_appendTo", null, rx); } to_appendTo.append(","); NumberBound<?> max = getMaxBound(); if(max == null) { to_appendTo.append("+inf)"); } else { to_appendTo.append(max.get()).append(max.isInclusive() ? "]" : ")"); } String minName = (!hasMin() ? null : getMinBound().getName()); String maxName = (!hasMax() ? null : getMinBound().getName()); if(minName != null || maxName != null) { to_appendTo.append(" ("); } if(minName != null) { to_appendTo.append("min=\"" + minName + "\""); } if(minName != null && maxName != null) { to_appendTo.append(", "); } if(maxName != null) { to_appendTo.append("max=\"" + maxName + "\""); } if(minName != null || maxName != null) { to_appendTo.append(")"); } return to_appendTo; } public String toString() { return getRules(); } public StringBuilder appendToString(StringBuilder to_appendTo) { return appendRules(to_appendTo); } /** * <p>If a bound is {@code null}, throw an {@code IllegalStateException}.</p> * @param bound The bound to check. * @param in_orAx If checking the minimum bound, set this to * "{@code in}". If maximum, "{@code ax}". */ protected void throwISXIfBoundNull(NumberBound<N> bound, String in_orAx) { if(bound == null) { throw new IllegalStateException("hasM" + in_orAx + "() is false."); } } public abstract void crashIfBadBoundsForCnstr(); //other...END public abstract boolean isGTOEMinGivenIncl(N num); public abstract boolean isLTOEMaxGivenIncl(N num); public N getInclMinComparedTo(N num) { try { return getMinBound().getInclComparedTo(BoundSide.MIN, num); } catch(RuntimeException rx) { CrashIfObject.nnull(getMinBound(), "getMinBound()", "hasMin() is false (actually " + hasMin() + ")"); throw CrashIfObject.nullOrReturnCause(num, "num", null, rx); } } public N getInclMaxComparedTo(N num) { try { return getMaxBound().getInclComparedTo(BoundSide.MAX, num); } catch(RuntimeException rx) { CrashIfObject.nnull(getMaxBound(), "getMaxBound()", "hasMax() is false (actually " + hasMax() + ")"); throw CrashIfObject.nullOrReturnCause(num, "num", null, rx); } } public abstract N getInclMinComparedToOrIfNoMin(N to_compareTo, N if_noMin); public abstract N getInclMaxComparedToOrIfNoMax(N to_compareTo, N if_noMax); public final boolean isIn(N num) { boolean bV4Min = false; try { bV4Min = (!hasMin() || isGTOEMinGivenIncl(num)); } catch(RuntimeException rx) { throw CrashIfObject.nullOrReturnCause(num, "num", null, rx); } boolean bV4Max = (!hasMax() || isLTOEMaxGivenIncl(num)); boolean isValidUninv = (bV4Min && bV4Max); return (!isInverted() ? isValidUninv : !isValidUninv); } /** * @return <code>true</code> If {@code to_compareTo} is * non-{@code null}, a {@code NumberInRange}, and all relevant fields * {@linkplain #areFieldsEqual(NumberInRange) are equal}. This is * implemented as suggested by Joshua Bloch in "Effective Java" * (2nd ed, item 8, page 46). */ @Override public boolean equals(Object to_compareTo) { //Check for object equality first, since it's faster than instanceof. if(this == to_compareTo) { return true; } if(!(to_compareTo instanceof NumberInRange)) { //to_compareTo is either null or not an NumberInRange. //java.lang.Object.object(o): "For any non-null reference value x, //x.equals(null) should return false." return false; } //Safe to cast @SuppressWarnings("unchecked") NumberInRange<N> o = (NumberInRange<N>)to_compareTo; //Finish with field-by-field comparison. return areFieldsEqual(o); } /** * Get a duplicate of this {@code NumberInRange}, with an opposite-value * inverted flag. * @return A duplicate of this object, whose {@link #isInverted() isInverted}{@code ()} * equal to {@code !this.isInverted()}. */ public abstract NumberInRange<N> getInvertedCopy(); /** * Get a new range containing the values in <i>both</i> * <i>{@code this}</i> and another range. * @param to_intersectWith May not be <code>null</code>. * @return If {@code to_intersectWith} * {@linkplain #doesOverlap(com.github.xbn.number.NumberInRange) overlaps} * <i>{@code this}</i> range, this returns a new range that * contains only those values in both. If they do not overlap, this * returns {@code null}. It is required that both * {@linkplain #getMinBound() min bounds} and both * {@linkplain #getMaxBound() max bounds} have the same inclusivity * (must both be {@linkplain Inclusive#YES inclusive} or both * {@linkplain Inclusive#NO exclusive}). (It is okay for one min (or * max) bound to exist, and the other not exist.) * @see #getMerged(NumberInRange, OverlapRequired) * @since 0.1.5 */ public abstract NumberInRange<N> getIntersection( NumberInRange<N> to_intersectWith); /** * Get a new range containing the values in <i>either</i> * <i>{@code this}</i> and another range. * @param to_mergeWith May not be <code>null</code>. If * <code>overlap_rqd.{@link OverlapRequired#YES YES}</code>, must * {@linkplain #doesOverlap(com.github.xbn.number.NumberInRange) overlaps} * <i>{@code this}</i> range. * @param overlap_rqd May not be <code>null</code>. * @return A new range whose values are in <i>either</i> * <i>{@code this}</i> or {@code to_mergeWith}. It is required that both * {@linkplain #getMinBound() min bounds} and both * {@linkplain #getMaxBound() max bounds} have the same inclusivity * (must both be {@linkplain Inclusive#YES inclusive} or both * {@linkplain Inclusive#NO exclusive}). (It is okay for one min (or max) * bound to exist, and the other not exist.) * @exception IllegalArgumentException If {@code overlap_rqd.YES} and * {@code to_mergeWith} does not overlap or touch <i>{@code this}</i> * one. * @see #getIntersection(NumberInRange) * @since 0.1.5 */ public abstract NumberInRange<N> getMerged(NumberInRange<N> to_mergeWith, OverlapRequired overlap_rqd); /** * Get the minimum bound, given inclusivity. * @return <code>{@link #getMinBound() getMinBound}().{@link NumberBound#getGivenIncl(BoundSide) getGivenIncl}({@link BoundSide}.{@link BoundSide#MIN MIN})</code> * @exception IllegalStateException If {@link #hasMin() hasMin}{@code ()} is {@code false}. * @see #getMaxGivenIncl() * @since 0.1.5 */ public N getMinGivenIncl() { try { return getMinBound().getGivenIncl(BoundSide.MIN); } catch(NullPointerException npx) { throw new IllegalStateException("hasMin() is false."); } } /** * Get the maximum bound, given inclusivity. * @return <code>{@link #getMaxBound() getMaxBound}().{@link NumberBound#getGivenIncl(BoundSide) getGivenIncl}({@link BoundSide}.{@link BoundSide#MAX MAX})</code> * @exception IllegalStateException If {@link #hasMax() hasMax}{@code ()} is {@code false}. * @see #getMinGivenIncl() * @since 0.1.5 */ public N getMaxGivenIncl() { try { return getMaxBound().getGivenIncl(BoundSide.MAX); } catch(NullPointerException npx) { throw new IllegalStateException("hasMax() is false."); } } /** * Does <i>{@code this}</i> range overlap another? * @param to_compareTo May not be <code>null</code>. * @return <blockquote><pre>(<!-- GENERIC PARAMETERS FAIL IN @link --><a href="#isIn(N)">isIn</a>(to_compareTo.{@link #getMinGivenIncl() getMinGivenIncl}()) || * isIn(to_compareTo.{@link #getMaxGivenIncl() getMaxGivenIncl}()()) || * to_compareTo.isIn(getMinGivenIncl()) || * to_compareTo.isIn(getMaxGivenIncl()))</pre></blockquote> * @see #getOverlapDebugging(NumberInRange) * @since 0.1.5 */ public boolean doesOverlap(NumberInRange<N> to_compareTo) { N inclMinThis = (!hasMin() ? null : getMinGivenIncl()); N inclMaxThis = (!hasMax() ? null : getMaxGivenIncl()); N inclMinThat = null; try { inclMinThat = (!to_compareTo.hasMin() ? null : to_compareTo.getMinGivenIncl()); } catch(NullPointerException npx) { throw new NullPointerException("to_compareTo"); } N inclMaxThat = (!to_compareTo.hasMax() ? null : to_compareTo.getMaxGivenIncl()); if(inclMinThis != null) { if(to_compareTo.isIn(inclMinThis)) { return true; } } if(inclMaxThis != null) { if(to_compareTo.isIn(inclMaxThis)) { return true; } } if(inclMinThat != null) { if(isIn(inclMinThat)) { return true; } } if(inclMaxThat != null) { if(isIn(inclMaxThat)) { return true; } } return (inclMinThis == null && inclMinThat == null && inclMaxThis == null && inclMaxThat == null); } public String getOverlapDebugging(NumberInRange<N> to_compareTo) { StringBuilder bldr = new StringBuilder("doesOverlap: this=" + this + ", to_compareTo=" + to_compareTo).append(LINE_SEP); if(to_compareTo.hasMin()) { bldr.append(" this.isIn(" + to_compareTo.getMinGivenIncl() + ")=" + isIn(to_compareTo.getMinGivenIncl())).append(LINE_SEP); } else { bldr.append("to_compareTo.hasMin() is false"); } if(to_compareTo.hasMax()) { bldr.append(" this.isIn(" + to_compareTo.getMaxGivenIncl() + ")=" + isIn(to_compareTo.getMaxGivenIncl())).append(LINE_SEP); } else { bldr.append("to_compareTo.hasMax() is false"); } if(hasMin()) { bldr.append(" to_compareTo.isIn(" + getMinGivenIncl() + ")=" + to_compareTo.isIn(getMinGivenIncl())).append(LINE_SEP); } else { bldr.append("this.hasMin() is false"); } if(hasMax()) { bldr.append(" to_compareTo.isIn(" + getMaxGivenIncl() + ")=" + to_compareTo.isIn(getMaxGivenIncl())).append(LINE_SEP); } else { bldr.append("this.hasMax() is false"); } return bldr.toString(); } /** * <p>Are all relevant fields equal?.</p> * @param to_compareTo May not be {@code null}. */ public abstract boolean areFieldsEqual(NumberInRange<N> to_compareTo); /** * @deprecated Use <code><!-- Generics fail in at-links: --><a href="#getDebuggingForIsIn(N)">getDebuggingForIsIn</a>(N)</code> * @since 0.1.5 */ public static final <N extends Number> String getValidityDebugging(NumberInRange<N> range, N to_validate, String to_vldtName) { try { return to_vldtName + "=" + to_validate + ", range=<" + range + ">, " + range.getDebuggingForIsIn(to_validate); } catch(NullPointerException npx) { throw new NullPointerException("range"); } } /** * In depth explanation on why a number is in or not in <i>{@code this}</i> range. * @param to_validate The number to analyze. May not be <code>null</code>. * @return Description on why {@code to_validate} is in or not in this range. This uses the terminology "valid" instead of "in". * @since 0.1.5 */ public String getDebuggingForIsIn(N to_validate) { return "valid-for-min?=" + (!hasMin() ? "yes (no min-bound)" : (!isGTOEMinGivenIncl(to_validate) ? "no (less than min, given its inclusive-ness)" : "yes (greater-than-or-equal-to min)")) + ", valid-for-max?=" + (!hasMax() ? "yes (no max-bound)" : (!isLTOEMaxGivenIncl(to_validate) ? "no (greater than max, given its inclusive-ness)" : "yes (greater-than-or-equal-to max)")) + ", valid entirely, NOT considering invertedness? " + (( (!hasMin() || isGTOEMinGivenIncl(to_validate)) && (!hasMax() || isLTOEMaxGivenIncl(to_validate))) ? "yes" : "no") + ", valid entirely, considering invertedness? " + (( (!hasMin() || isGTOEMinGivenIncl(to_validate)) && (!hasMax() || isLTOEMaxGivenIncl(to_validate)) && isInverted()) ? "yes" : "no"); } }