All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vladmihalcea.hibernate.type.range.Range Maven / Gradle / Ivy

There is a newer version: 2.21.1
Show newest version
package com.vladmihalcea.hibernate.type.range;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.Objects;
import java.util.function.Function;

/**
 * Represents the range/interval with two bounds. Abstraction follows the semantics of the mathematical interval. The
 * range can be unbounded or open from the left or/and unbounded from the right. The range supports half-open or closed
 * bounds on both sides.
 *
 * 

* The class has some very simple methods for usability. For example {@link Range#contains(Comparable)} method can tell user whether * this range contains argument or not. The {@link Range#contains(Range)} helps to find out whether this range fully * enclosing argument or not. *

* For more details about how to use it, * check out this article * on vladmihalcea.com. * * @author Edgar Asatryan * @author Vlad Mihalcea */ public final class Range implements Serializable { public static final int LOWER_INCLUSIVE = 1 << 1; public static final int LOWER_EXCLUSIVE = 1 << 2; public static final int UPPER_INCLUSIVE = 1 << 3; public static final int UPPER_EXCLUSIVE = 1 << 4; public static final int LOWER_INFINITE = (1 << 5) | LOWER_EXCLUSIVE; public static final int UPPER_INFINITE = (1 << 6) | UPPER_EXCLUSIVE; public static final String EMPTY = "empty"; public static final String INFINITY = "infinity"; private static final DateTimeFormatter LOCAL_DATE_TIME = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd HH:mm:ss") .optionalStart() .appendPattern(".") .appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false) .optionalEnd() .toFormatter(); private static final DateTimeFormatter ZONE_DATE_TIME = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd HH:mm:ss") .optionalStart() .appendPattern(".") .appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false) .optionalEnd() .appendOffset("+HH:mm", "Z") .toFormatter(); private final T lower; private final T upper; private final int mask; private final Class clazz; private Range(T lower, T upper, int mask, Class clazz) { this.lower = lower; this.upper = upper; this.mask = mask; this.clazz = clazz; if (isBounded() && lower.compareTo(upper) > 0) { throw new IllegalArgumentException("The lower bound is greater then upper!"); } } /** * Creates the closed range with provided bounds. *

* The mathematical equivalent will be: *

{@code
     *     [a, b] = {x | a <= x <= b}
     * }
. * * @param lower The lower bound, never null. * @param upper The upper bound, never null. * @param The type of bounds. * * @return The closed range. */ @SuppressWarnings("unchecked") public static > Range closed(T lower, T upper) { Objects.requireNonNull(lower); Objects.requireNonNull(upper); return new Range<>(lower, upper, LOWER_INCLUSIVE | UPPER_INCLUSIVE, (Class) lower.getClass()); } /** * Creates the open range with provided bounds. *

* The mathematical equivalent will be: *

{@code
     *     (a, b) = {x | a < x < b}
     * }
* * @param lower The lower bound, never null. * @param upper The upper bound, never null. * @param The type of bounds. * * @return The range. */ @SuppressWarnings("unchecked") public static > Range open(T lower, T upper) { Objects.requireNonNull(lower); Objects.requireNonNull(upper); return new Range<>(lower, upper, LOWER_EXCLUSIVE | UPPER_EXCLUSIVE, (Class) lower.getClass()); } /** * Creates the left-open, right-closed range with provided bounds. *

* The mathematical equivalent will be: *

{@code
     *     (a, b] = {x | a < x <= b}
     * }
* * @param lower The lower bound, never null. * @param upper The upper bound, never null. * @param The type of bounds. * * @return The range. */ @SuppressWarnings("unchecked") public static > Range openClosed(T lower, T upper) { Objects.requireNonNull(lower); Objects.requireNonNull(upper); return new Range<>(lower, upper, LOWER_EXCLUSIVE | UPPER_INCLUSIVE, (Class) lower.getClass()); } /** * Creates the left-closed, right-open range with provided bounds. *

* The mathematical equivalent will be: *

{@code
     *     [a, b) = {x | a <= x < b}
     * }
* * @param lower The lower bound, never null. * @param upper The upper bound, never null. * @param The type of bounds. * * @return The range. */ @SuppressWarnings("unchecked") public static > Range closedOpen(T lower, T upper) { Objects.requireNonNull(lower); Objects.requireNonNull(upper); return new Range<>(lower, upper, LOWER_INCLUSIVE | UPPER_EXCLUSIVE, (Class) lower.getClass()); } /** * Creates the left-bounded, left-open and right-unbounded range with provided lower bound. *

* The mathematical equivalent will be: *

{@code
     *     (a, +∞) = {x | x > a}
     * }
* * @param lower The lower bound, never null. * @param The type of bounds. * * @return The range. */ @SuppressWarnings("unchecked") public static > Range openInfinite(T lower) { Objects.requireNonNull(lower); return new Range<>(lower, null, LOWER_EXCLUSIVE | UPPER_INFINITE, (Class) lower.getClass()); } /** * Creates the left-bounded, left-closed and right-unbounded range with provided lower bound. *

* The mathematical equivalent will be: *

{@code
     *     [a, +∞) = {x | x >= a}
     * }
* * @param lower The lower bound, never null. * @param The type of bounds. * * @return The range. */ @SuppressWarnings("unchecked") public static > Range closedInfinite(T lower) { Objects.requireNonNull(lower); return new Range(lower, null, LOWER_INCLUSIVE | UPPER_INFINITE, lower.getClass()); } /** * Creates the left-unbounded, right-bounded and right-open range with provided upper bound. *

* The mathematical equivalent will be: *

{@code
     *     (-∞, b) = {x | x < b}
     * }
* * @param upper The upper bound, never null. * @param The type of bounds. * * @return The range. */ @SuppressWarnings("unchecked") public static > Range infiniteOpen(T upper) { Objects.requireNonNull(upper); return new Range<>(null, upper, UPPER_EXCLUSIVE | LOWER_INFINITE, (Class) upper.getClass()); } /** * Creates the left-unbounded, right-bounded and right-closed range with provided upper bound. *

* The mathematical equivalent will be: *

{@code
     *     (-∞, b] = {x | x =< b}
     * }
* * @param upper The upper bound, never null. * @param The type of bounds. * * @return The range. */ @SuppressWarnings("unchecked") public static > Range infiniteClosed(T upper) { Objects.requireNonNull(upper); return new Range<>(null, upper, UPPER_INCLUSIVE | LOWER_INFINITE, (Class) upper.getClass()); } /** * Creates the unbounded at both ends range with provided upper bound. *

* The mathematical equivalent will be: *

{@code
     *     (-∞, +∞) = ℝ
     * }
* * @param cls The range class, never null. * @param The type of bounds. * * @return The infinite range. */ @SuppressWarnings("unchecked") public static > Range infinite(Class cls) { return new Range<>(null, null, LOWER_INFINITE | UPPER_INFINITE, cls); } @SuppressWarnings("unchecked") public static Range ofString(String str, Function converter, Class clazz) { if(str.equals(EMPTY)) { return emptyRange(clazz); } int mask = str.charAt(0) == '[' ? LOWER_INCLUSIVE : LOWER_EXCLUSIVE; mask |= str.charAt(str.length() - 1) == ']' ? UPPER_INCLUSIVE : UPPER_EXCLUSIVE; int delim = str.indexOf(','); if (delim == -1) { throw new IllegalArgumentException("Cannot find comma character"); } String lowerStr = str.substring(1, delim); String upperStr = str.substring(delim + 1, str.length() - 1); if (lowerStr.length() == 0 || lowerStr.endsWith(INFINITY)) { mask |= LOWER_INFINITE; } if (upperStr.length() == 0 || upperStr.endsWith(INFINITY)) { mask |= UPPER_INFINITE; } T lower = null; T upper = null; if ((mask & LOWER_INFINITE) != LOWER_INFINITE) { lower = converter.apply(lowerStr); } if ((mask & UPPER_INFINITE) != UPPER_INFINITE) { upper = converter.apply(upperStr); } return new Range<>(lower, upper, mask, clazz); } /** * Creates the {@code BigDecimal} range from provided string: *
{@code
     *     Range closed = Range.bigDecimalRange("[0.1,1.1]");
     *     Range halfOpen = Range.bigDecimalRange("(0.1,1.1]");
     *     Range open = Range.bigDecimalRange("(0.1,1.1)");
     *     Range leftUnbounded = Range.bigDecimalRange("(,1.1)");
     * }
* * @param range The range string, for example {@literal "[5.5,7.8]"}. * * @return The range of {@code BigDecimal}s. * * @throws NumberFormatException when one of the bounds are invalid. */ public static Range bigDecimalRange(String range) { return ofString(range, BigDecimal::new, BigDecimal.class); } /** * Creates the {@code Integer} range from provided string: *
{@code
     *     Range closed = Range.integerRange("[1,5]");
     *     Range halfOpen = Range.integerRange("(-1,1]");
     *     Range open = Range.integerRange("(1,2)");
     *     Range leftUnbounded = Range.integerRange("(,10)");
     *     Range unbounded = Range.integerRange("(,)");
     * }
* * @param range The range string, for example {@literal "[5,7]"}. * * @return The range of {@code Integer}s. * * @throws NumberFormatException when one of the bounds are invalid. */ public static Range integerRange(String range) { return ofString(range, Integer::parseInt, Integer.class); } /** * Creates the {@code Long} range from provided string: *
{@code
     *     Range closed = Range.longRange("[1,5]");
     *     Range halfOpen = Range.longRange("(-1,1]");
     *     Range open = Range.longRange("(1,2)");
     *     Range leftUnbounded = Range.longRange("(,10)");
     *     Range unbounded = Range.longRange("(,)");
     * }
* * @param range The range string, for example {@literal "[5,7]"}. * * @return The range of {@code Long}s. * * @throws NumberFormatException when one of the bounds are invalid. */ public static Range longRange(String range) { return ofString(range, Long::parseLong, Long.class); } /** * Creates the {@code LocalDateTime} range from provided string: *
{@code
     *     Range closed = Range.localDateTimeRange("[2014-04-28 16:00:49,2015-04-28 16:00:49]");
     *     Range quoted = Range.localDateTimeRange("[\"2014-04-28 16:00:49\",\"2015-04-28 16:00:49\"]");
     *     Range iso = Range.localDateTimeRange("[\"2014-04-28T16:00:49.2358\",\"2015-04-28T16:00:49\"]");
     * }
*

* The valid formats for bounds are: *

    *
  • yyyy-MM-dd HH:mm:ss[.SSSSSS]
  • *
  • yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]
  • *
* * @param range The range string, for example {@literal "[2014-04-28 16:00:49,2015-04-28 16:00:49]"}. * * @return The range of {@code LocalDateTime}s. * * @throws DateTimeParseException when one of the bounds are invalid. */ public static Range localDateTimeRange(String range) { return ofString(range, parseLocalDateTime().compose(unquote()), LocalDateTime.class); } /** * Creates the {@code LocalDate} range from provided string: *
{@code
     *     Range closed = Range.localDateRange("[2014-04-28,2015-04-289]");
     *     Range quoted = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
     *     Range iso = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
     * }
*

* The valid formats for bounds are: *

    *
  • yyyy-MM-dd
  • *
  • yyyy-MM-dd
  • *
* * @param range The range string, for example {@literal "[2014-04-28,2015-04-28]"}. * * @return The range of {@code LocalDate}s. * * @throws DateTimeParseException when one of the bounds are invalid. */ public static Range localDateRange(String range) { Function parseLocalDate = LocalDate::parse; return ofString(range, parseLocalDate.compose(unquote()), LocalDate.class); } /** * Creates the {@code ZonedDateTime} range from provided string: *
{@code
     *     Range closed = Range.zonedDateTimeRange("[2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00]");
     *     Range quoted = Range.zonedDateTimeRange("[\"2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00\"]");
     *     Range iso = Range.zonedDateTimeRange("[2011-12-03T10:15:30+01:00[Europe/Paris], 2012-12-03T10:15:30+01:00[Europe/Paris]]");
     * }
*

* The valid formats for bounds are: *

    *
  • yyyy-MM-dd HH:mm:ss[.SSSSSS]X
  • *
  • yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X
  • *
* * @param rangeStr The range string, for example {@literal "[2011-12-03T10:15:30+01:00,2012-12-03T10:15:30+01:00]"}. * * @return The range of {@code ZonedDateTime}s. * * @throws DateTimeParseException when one of the bounds are invalid. * @throws IllegalArgumentException when bounds time zones are different. */ public static Range zonedDateTimeRange(String rangeStr) { Range range = ofString(rangeStr, parseZonedDateTime().compose(unquote()), ZonedDateTime.class); if (range.hasLowerBound() && range.hasUpperBound()) { ZoneId lowerZone = range.lower().getZone(); ZoneId upperZone = range.upper().getZone(); if (!lowerZone.equals(upperZone)) { Duration lowerDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.lower().toInstant()); Duration upperDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.upper().toInstant()); long dstSeconds = upperDst.minus(lowerDst).getSeconds(); if(dstSeconds < 0 ) { dstSeconds *= -1; } long zoneDriftSeconds = ((ZoneOffset) lowerZone).getTotalSeconds() - ((ZoneOffset) upperZone).getTotalSeconds(); if (zoneDriftSeconds < 0) { zoneDriftSeconds *= -1; } if (dstSeconds != zoneDriftSeconds) { throw new IllegalArgumentException("The upper and lower bounds must be in same time zone!"); } } } return range; } private static Function parseLocalDateTime() { return s -> { try { return LocalDateTime.parse(s, LOCAL_DATE_TIME); } catch (DateTimeParseException e) { return LocalDateTime.parse(s); } }; } private static Function parseZonedDateTime() { return s -> { try { return ZonedDateTime.parse(s, ZONE_DATE_TIME); } catch (DateTimeParseException e) { return ZonedDateTime.parse(s); } }; } private static Function unquote() { return s -> { if (s.charAt(0) == '\"' && s.charAt(s.length() - 1) == '\"') { return s.substring(1, s.length() - 1); } return s; }; } private boolean isBounded() { return !hasMask(LOWER_INFINITE) && !hasMask(UPPER_INFINITE); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Range)) return false; Range range = (Range) o; return mask == range.mask && Objects.equals(lower, range.lower) && Objects.equals(upper, range.upper) && Objects.equals(clazz, range.clazz); } @Override public int hashCode() { return Objects.hash(lower, upper, mask, clazz); } @Override public String toString() { return "Range{" + "lower=" + lower + ", upper=" + upper + ", mask=" + mask + ", clazz=" + clazz + '}'; } public boolean hasMask(int flag) { return (mask & flag) == flag; } public boolean isLowerBoundClosed() { return hasLowerBound() && hasMask(LOWER_INCLUSIVE); } public boolean isUpperBoundClosed() { return hasUpperBound() && hasMask(UPPER_INCLUSIVE); } public boolean hasLowerBound() { return !hasMask(LOWER_INFINITE); } public boolean hasUpperBound() { return !hasMask(UPPER_INFINITE); } /** * Returns the lower bound of this range. If {@code null} is returned then this range is left-unbounded. * * @return The lower bound. */ public T lower() { return lower; } /** * Returns the upper bound of this range. If {@code null} is returned then this range is right-unbounded. * * @return The upper bound. */ public T upper() { return upper; } /** * Determines whether this range contains this point or not. *

* For example: *

{@code
     *     assertTrue(integerRange("[1,2]").contains(1))
     *     assertTrue(integerRange("[1,2]").contains(2))
     *     assertTrue(integerRange("[-1,1]").contains(0))
     *     assertTrue(infinity(Integer.class).contains(Integer.MAX_VALUE))
     *     assertTrue(infinity(Integer.class).contains(Integer.MIN_VALUE))
     *
     *     assertFalse(integerRange("(1,2]").contains(1))
     *     assertFalse(integerRange("(1,2]").contains(3))
     *     assertFalse(integerRange("[-1,1]").contains(0))
     * }
* * @param point The point to check. * * @return Whether {@code point} in this range or not. */ @SuppressWarnings("unchecked") public boolean contains(T point) { boolean l = hasLowerBound(); boolean u = hasUpperBound(); if (l && u) { boolean inLower = hasMask(LOWER_INCLUSIVE) ? lower.compareTo(point) <= 0 : lower.compareTo(point) < 0; boolean inUpper = hasMask(UPPER_INCLUSIVE) ? upper.compareTo(point) >= 0 : upper.compareTo(point) > 0; return inLower && inUpper; } else if (l) { return hasMask(LOWER_INCLUSIVE) ? lower.compareTo(point) <= 0 : lower.compareTo(point) < 0; } else if (u) { return hasMask(UPPER_INCLUSIVE) ? upper.compareTo(point) >= 0 : upper.compareTo(point) > 0; } // INFINITY return true; } /** * Determines whether this range contains this point or not. *

* For example: *

{@code
     *     assertTrue(integerRange("[-2,2]").contains(integerRange("[-1,1]")))
     *     assertTrue(integerRange("(,)").contains(integerRange("(,)"))
     *
     *     assertFalse(integerRange("[-2,2)").contains(integerRange("[-1,2]")))
     *     assertFalse(integerRange("(-2,2]").contains(1))
     * }
* * @param range The range to check. * * @return Whether {@code range} in this range or not. */ public boolean contains(Range range) { return (!range.hasLowerBound() || contains(range.lower)) && (!range.hasUpperBound() || contains(range.upper)); } public String asString() { StringBuilder sb = new StringBuilder(); sb.append(hasMask(LOWER_INCLUSIVE) ? '[' : '(') .append(hasLowerBound() ? boundToString().apply(lower) : "") .append(",") .append(hasUpperBound() ? boundToString().apply(upper) : "") .append(hasMask(UPPER_INCLUSIVE) ? ']' : ')'); return sb.toString(); } private Function boundToString() { return t -> { if (clazz.equals(ZonedDateTime.class)) { return ZONE_DATE_TIME.format((ZonedDateTime) t); } return t.toString(); }; } Class getClazz() { return clazz; } public static > Range emptyRange(Class clazz) { return new Range( null, null, LOWER_INFINITE|UPPER_INFINITE, clazz ); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy