/* * Copyright 2016 Red Hat, Inc. and/or its affiliates. * * 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 org.optaplanner.core.impl.domain.valuerange.buildin.temporal; import java.time.DateTimeException; import java.time.temporal.Temporal; import java.time.temporal.TemporalAmount; import java.time.temporal.TemporalUnit; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Random; import org.optaplanner.core.impl.domain.valuerange.AbstractCountableValueRange; import org.optaplanner.core.impl.domain.valuerange.util.ValueRangeIterator; import org.optaplanner.core.impl.solver.random.RandomUtils; public class TemporalValueRange<Temporal_ extends Temporal & Comparable<? super Temporal_>> extends AbstractCountableValueRange<Temporal_> { private final Temporal_ from; private final Temporal_ to; /** We could not use a {@link TemporalAmount} as {@code incrementUnit} due to lack of calculus functions. */ private final long incrementUnitAmount; private final TemporalUnit incrementUnitType; private final long size; /** * @param from never null, inclusive minimum * @param to never null, exclusive maximum, {@code >= from} * @param incrementUnitAmount {@code > 0} * @param incrementUnitType never null, must be {@link Temporal#isSupported(TemporalUnit) supported} by {@code from} * and {@code to} */ public TemporalValueRange(Temporal_ from, Temporal_ to, long incrementUnitAmount, TemporalUnit incrementUnitType) { this.from = from; this.to = to; this.incrementUnitAmount = incrementUnitAmount; this.incrementUnitType = incrementUnitType; if (from == null || to == null || incrementUnitType == null) { throw new IllegalArgumentException("The " + getClass().getSimpleName() + " must have a from (" + from + "), to (" + to + ") and incrementUnitType (" + incrementUnitType + ") that are not null."); } if (incrementUnitAmount <= 0) { throw new IllegalArgumentException("The " + getClass().getSimpleName() + " must have strictly positive incrementUnitAmount (" + incrementUnitAmount + ")."); } if (!from.isSupported(incrementUnitType) || !to.isSupported(incrementUnitType)) { throw new IllegalArgumentException("The " + getClass().getSimpleName() + " must have an incrementUnitType (" + incrementUnitType + ") that is supported by its from (" + from + ") class (" + from.getClass().getSimpleName() + ") and to (" + to + ") class (" + to.getClass().getSimpleName() + ")."); } // We cannot use Temporal.until() to check bounds due to rounding errors if (from.compareTo(to) > 0) { throw new IllegalArgumentException("The " + getClass().getSimpleName() + " cannot have a from (" + from + ") which is strictly higher than its to (" + to + ")."); } long space = from.until(to, incrementUnitType); Temporal expectedTo = from.plus(space, incrementUnitType); if (!to.equals(expectedTo)) { // Temporal.until() rounds down, but it needs to round up, to be consistent with Temporal.plus() space++; Temporal roundedExpectedTo; try { roundedExpectedTo = from.plus(space, incrementUnitType); } catch (DateTimeException e) { throw new IllegalArgumentException("The " + getClass().getSimpleName() + "'s incrementUnitType (" + incrementUnitType + ") must fit an integer number of times in the space (" + space + ") between from (" + from + ") and to (" + to + ").\n" + "The to (" + to + ") is not the expectedTo (" + expectedTo + ").", e); } // Fail fast if there's a remainder on type (to be consistent with other value ranges) if (!to.equals(roundedExpectedTo)) { throw new IllegalArgumentException("The " + getClass().getSimpleName() + "'s incrementUnitType (" + incrementUnitType + ") must fit an integer number of times in the space (" + space + ") between from (" + from + ") and to (" + to + ").\n" + "The to (" + to + ") is not the expectedTo (" + expectedTo + ") nor the roundedExpectedTo (" + roundedExpectedTo + ")."); } } // Fail fast if there's a remainder on amount (to be consistent with other value ranges) if (space % incrementUnitAmount > 0) { throw new IllegalArgumentException("The " + getClass().getSimpleName() + "'s incrementUnitAmount (" + incrementUnitAmount + ") must fit an integer number of times in the space (" + space + ") between from (" + from + ") and to (" + to + ")."); } size = space / incrementUnitAmount; } @Override public long getSize() { return size; } @Override public Temporal_ get(long index) { if (index >= size || index < 0) { throw new IndexOutOfBoundsException(); } return (Temporal_) from.plus(index * incrementUnitAmount, incrementUnitType); } @Override public boolean contains(Temporal_ value) { if (value == null || !value.isSupported(incrementUnitType)) { return false; } // We cannot use Temporal.until() to check bounds due to rounding errors if (value.compareTo(from) < 0 || value.compareTo(to) >= 0) { return false; } long fromSpace = from.until(value, incrementUnitType); if (value.equals(from.plus(fromSpace + 1, incrementUnitType))) { // Temporal.until() rounds down, but it needs to round up, to be consistent with Temporal.plus() fromSpace++; } // Only checking the modulus is not enough: 1-MAR + 1 month doesn't include 7-MAR but the modulus is 0 anyway return fromSpace % incrementUnitAmount == 0 && value.equals(from.plus(fromSpace, incrementUnitType)); } @Override public Iterator<Temporal_> createOriginalIterator() { return new OriginalTemporalValueRangeIterator(); } private class OriginalTemporalValueRangeIterator extends ValueRangeIterator<Temporal_> { private long index = 0L; @Override public boolean hasNext() { return index < size; } @Override public Temporal_ next() { if (index >= size) { throw new NoSuchElementException(); } // Do not use upcoming += incrementUnitAmount because 31-JAN + 1 month + 1 month returns 28-MAR Temporal_ next = get(index); index++; return next; } } @Override public Iterator<Temporal_> createRandomIterator(Random workingRandom) { return new RandomTemporalValueRangeIterator(workingRandom); } private class RandomTemporalValueRangeIterator extends ValueRangeIterator<Temporal_> { private final Random workingRandom; public RandomTemporalValueRangeIterator(Random workingRandom) { this.workingRandom = workingRandom; } @Override public boolean hasNext() { return size > 0L; } @Override public Temporal_ next() { long index = RandomUtils.nextLong(workingRandom, size); return get(index); } } @Override public String toString() { return "[" + from + "-" + to + ")"; // Formatting: interval (mathematics) ISO 31-11 } }