Segment.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2024, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 * 
 * If you like this project or if you find it useful, you can support us at:
 * 
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 * 
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques, Mario Kušek
 * 
 *
 */
package net.sourceforge.plantuml.project.ngm.math;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;

/**
 * A constant workload segment over a time interval.
 *
 * <p>
 * A segment represents a time interval where the workload value remains
 * constant. The boundary convention is: <strong>both bounds are
 * exclusive</strong>, denoted as {@code ]start, end[} (or equivalently
 * {@code (start, end)} in some notations). This symmetric design simplifies
 * bidirectional iteration.
 * </p>
 *
 * <p>
 * Segments can be either {@link TimeDirection#FORWARD} or
 * {@link TimeDirection#BACKWARD}:
 * </p>
 * <ul>
 * <li><strong>FORWARD</strong>: {@code start} is chronologically before
 * {@code end} (used for forward iteration through time)</li>
 * <li><strong>BACKWARD</strong>: {@code start} is chronologically after
 * {@code end} (used for backward iteration through time)</li>
 * </ul>
 *
 * <p>
 * In both cases, both {@code start} and {@code end} are exclusive bounds. This
 * symmetric convention ensures that boundary instants are never "inside" a
 * segment, which simplifies reasoning about segment adjacency and splitting.
 * </p>
 *
 * <p>
 * Think of it as a time traveler: they travel from {@code start} toward
 * {@code end}, but never actually stand on either boundary instant. The segment
 * contains all instants strictly between the two bounds. For FORWARD segments,
 * travel is into the future. For BACKWARD segments, travel is into the past.
 * </p>
 *
 * <p>
 * The value may be zero to explicitly model periods with no assigned workload.
 * </p>
 *
 * <p>
 * Instances are created via the factory methods
 * {@link #forward(LocalDateTime, LocalDateTime, Fraction)} and
 * {@link #backward(LocalDateTime, LocalDateTime, Fraction)}.
 * </p>
 *
 * @see TimeDirection
 * @see Fraction
 */
public final class Segment {

	private final LocalDateTime start;

	private final LocalDateTime end;

	/**
	 * The constant workload value over this segment.
	 */
	private final Fraction value;

	private final TimeDirection direction;

	/**
	 * Private constructor. Use {@link #forward} or {@link #backward} factory
	 * methods.
	 */
	private Segment(TimeDirection direction, LocalDateTime start, LocalDateTime end, Fraction value) {
		Objects.requireNonNull(value);

		this.direction = direction;
		this.start = start;
		this.end = end;
		this.value = value;
	}

	/**
	 * Creates a forward segment where {@code start} is chronologically before
	 * {@code end}.
	 *
	 * <p>
	 * The segment represents the time interval {@code ]start, end[} for forward
	 * iteration. Both bounds are exclusive.
	 * </p>
	 *
	 * @param start the exclusive start bound (chronologically earlier)
	 * @param end   the exclusive end bound (chronologically later)
	 * @param value the constant workload value for this segment
	 * @return a new forward segment
	 * @throws NullPointerException     if any argument is {@code null}
	 * @throws IllegalArgumentException if {@code start} is not chronologically
	 *                                  before {@code end}
	 */
	public static Segment forward(LocalDateTime start, LocalDateTime end, Fraction value) {
		if (start.isBefore(end) == false)
			throw new IllegalArgumentException("end must be after start");
		return new Segment(TimeDirection.FORWARD, start, end, value);
	}

	/**
	 * Creates a backward segment where {@code start} is chronologically after
	 * {@code end}.
	 *
	 * <p>
	 * The segment represents the time interval {@code ]start, end[} for backward
	 * iteration, where {@code start} is the later instant and {@code end} is the
	 * earlier instant. Both bounds are exclusive.
	 * </p>
	 *
	 *
	 * @param start the exclusive start bound (chronologically later)
	 * @param end   the exclusive end bound (chronologically earlier)
	 * @param value the constant workload value for this segment
	 * @return a new backward segment
	 * @throws NullPointerException     if any argument is {@code null}
	 * @throws IllegalArgumentException if {@code end} is not chronologically before
	 *                                  {@code start}
	 */
	public static Segment backward(LocalDateTime start, LocalDateTime end, Fraction value) {
		if (end.isBefore(start) == false)
			throw new IllegalArgumentException("end must be before start");
		return new Segment(TimeDirection.BACKWARD, start, end, value);
	}

	/**
	 * Returns the direction of this segment.
	 *
	 * @return {@link TimeDirection#FORWARD} if {@code start} is chronologically
	 *         before {@code end}, {@link TimeDirection#BACKWARD} if {@code start}
	 *         is chronologically after {@code end}
	 */
	public TimeDirection getTimeDirection() {
		return direction;
	}

	/**
	 * Returns the exclusive start bound of this segment.
	 *
	 * <p>
	 * For {@link TimeDirection#FORWARD} segments, this is the chronologically
	 * earlier instant. For {@link TimeDirection#BACKWARD} segments, this is the
	 * chronologically later instant.
	 * </p>
	 *
	 * <p>
	 * This bound is exclusive: the returned instant is not considered part of the
	 * segment.
	 * </p>
	 *
	 * @return the exclusive start bound
	 */
	public LocalDateTime startExclusive() {
		return start;
	}

	/**
	 * Returns the exclusive end bound of this segment.
	 *
	 * <p>
	 * For {@link TimeDirection#FORWARD} segments, this is the chronologically later
	 * instant. For {@link TimeDirection#BACKWARD} segments, this is the
	 * chronologically earlier instant.
	 * </p>
	 *
	 * <p>
	 * This bound is exclusive: the returned instant is not considered part of the
	 * segment.
	 * </p>
	 *
	 * @return the exclusive end bound
	 */
	public LocalDateTime endExclusive() {
		return end;
	}

	/**
	 * Returns the constant workload value of this segment.
	 *
	 * @return the workload value, never {@code null}
	 */
	public Fraction getValue() {
		return value;
	}

	/**
	 * Splits this segment into two contiguous segments at the given instant.
	 *
	 * <p>
	 * The split instant {@code time} must lie strictly within the bounds of this
	 * segment (exclusive of both {@code start} and {@code end}).
	 * </p>
	 *
	 * <p>
	 * The returned array always contains exactly two segments with the same
	 * {@link #getTimeDirection() direction} as this segment:
	 * </p>
	 * <ul>
	 * <li>the first segment spans from {@code start} (exclusive) to {@code time}
	 * (exclusive),</li>
	 * <li>the second segment spans from {@code time} (exclusive) to {@code end}
	 * (exclusive).</li>
	 * </ul>
	 *
	 * <p>
	 * Both resulting segments carry the same {@link #getValue() value} as this
	 * segment. The two segments are guaranteed to be contiguous and
	 * non-overlapping.
	 * </p>
	 *
	 * @param time the instant at which to split this segment
	 * @return an array of exactly two segments resulting from the split
	 * @throws NullPointerException     if {@code time} is {@code null}
	 * @throws IllegalArgumentException if {@code time} lies outside the bounds of
	 *                                  this segment or is equal to {@code start} or
	 *                                  {@code end}
	 */
	public Segment[] split(LocalDateTime time) {
		Objects.requireNonNull(time, "time must not be null");
		if (includes(time) == false)
			throw new IllegalArgumentException("time must be within the segment bounds");

		if (direction == TimeDirection.FORWARD) {
			final Segment first = Segment.forward(start, time, value);
			final Segment second = Segment.forward(time, end, value);
			return new Segment[] { first, second };
		} else {
			final Segment first = Segment.backward(start, time, value);
			final Segment second = Segment.backward(time, end, value);
			return new Segment[] { first, second };
		}
	}

	/**
	 * Checks whether the given instant lies strictly within this segment.
	 *
	 * <p>
	 * This method excludes both boundaries. The check is {@code (start, end)} —
	 * exclusive on both ends.
	 * </p>
	 * <ul>
	 * <li>{@link TimeDirection#FORWARD}: {@code start < time < end}</li>
	 * <li>{@link TimeDirection#BACKWARD}: {@code end < time < start}</li>
	 * </ul>
	 *
	 * @param time the instant to check
	 * @return {@code true} if {@code time} lies strictly between {@code start} and
	 *         {@code end} (exclusive on both ends), {@code false} otherwise
	 * @throws NullPointerException if {@code time} is {@code null}
	 */
	public boolean includes(LocalDateTime time) {
		Objects.requireNonNull(time, "time must not be null");

		if (direction == TimeDirection.FORWARD)
			return time.isAfter(start) && time.isBefore(end);
		else
			return time.isBefore(start) && time.isAfter(end);

	}

	/**
	 * Returns a string representation of this segment.
	 *
	 * <p>
	 * The format is: {@code DIRECTION ]start, end[ value=V} where DIRECTION is
	 * either FORWARD or BACKWARD.
	 * </p>
	 *
	 * @return a string representation of this segment
	 */
	@Override
	public String toString() {
		return getTimeDirection() + " ]" + start + ", " + end + "[ value=" + value;
	}

	/**
	 * Computes the intersection of multiple segments using multiplication to
	 * combine their values.
	 *
	 * <p>
	 * The intersection is defined as the overlapping time range shared by all
	 * provided segments. If no such overlapping range exists, an exception is
	 * thrown.
	 * </p>
	 *
	 * <p>
	 * The value of the resulting segment is computed by multiplying the values of
	 * all intersecting segments.
	 * </p>
	 *
	 * @param segments the list of segments to intersect
	 * @return a new {@link Segment} representing the intersection of the input
	 *         segments with the combined value
	 * @throws IllegalArgumentException if {@code segments} is empty or if no
	 *                                  overlapping range exists among the segments
	 * @throws NullPointerException     if {@code segments} is {@code null}
	 */
	public static Segment intersection(List<Segment> segments) {
		return intersection(segments, Fraction.PRODUCT);
	}

	/**
	 * Computes the intersection of multiple segments.
	 *
	 * <p>
	 * The intersection is defined as the overlapping time range shared by all
	 * provided segments. If no such overlapping range exists, an exception is
	 * thrown.
	 * </p>
	 *
	 * <p>
	 * All segments must have the same {@link TimeDirection}. The resulting segment
	 * will have the same direction as the input segments.
	 * </p>
	 *
	 * <p>
	 * The value of the resulting segment is computed by applying the provided
	 * {@code valueFunction} to the values of all intersecting segments.
	 * </p>
	 *
	 * @param segments      the list of segments to intersect
	 * @param valueFunction a function that combines two {@link Fraction} values
	 *                      into one; this function is applied iteratively to
	 *                      combine the values of all intersecting segments
	 * @return a new {@link Segment} representing the intersection of the input
	 *         segments with the combined value
	 * @throws IllegalArgumentException if {@code segments} is empty, if segments
	 *                                  have different directions, or if no
	 *                                  overlapping range exists among the segments
	 * @throws NullPointerException     if {@code segments} or {@code valueFunction}
	 *                                  is {@code null}
	 */
	public static Segment intersection(List<Segment> segments, BiFunction<Fraction, Fraction, Fraction> valueFunction) {
		Objects.requireNonNull(valueFunction, "valueFunction must not be null");

		if (segments.isEmpty())
			throw new IllegalArgumentException("No segments to intersect");

		if (segments.size() == 1)
			return segments.get(0);

		final TimeDirection direction = segments.get(0).getTimeDirection();

		LocalDateTime start = segments.get(0).startExclusive();
		LocalDateTime end = segments.get(0).endExclusive();
		Fraction combinedValue = segments.get(0).getValue();

		for (int i = 1; i < segments.size(); i++) {
			final Segment segment = segments.get(i);

			if (segment.getTimeDirection() != direction)
				throw new IllegalArgumentException("All segments must have the same direction");

			if (direction == TimeDirection.FORWARD) {
				if (segment.startExclusive().isAfter(start))
					start = segment.startExclusive();
				if (segment.endExclusive().isBefore(end))
					end = segment.endExclusive();
			} else {
				if (segment.startExclusive().isBefore(start))
					start = segment.startExclusive();
				if (segment.endExclusive().isAfter(end))
					end = segment.endExclusive();
			}

			combinedValue = valueFunction.apply(combinedValue, segment.getValue());
		}

		if (direction == TimeDirection.FORWARD) {
			if (start.isBefore(end) == false)
				throw new IllegalArgumentException("Segments do not overlap");
			return Segment.forward(start, end, combinedValue);
		} else {
			if (end.isBefore(start) == false)
				throw new IllegalArgumentException("Segments do not overlap");
			return Segment.backward(start, end, combinedValue);
		}
	}

	/**
	 * Computes the intersection of multiple segments using multiplication to
	 * combine their values.
	 *
	 * <p>
	 * Convenience overload for array inputs. Delegates to
	 * {@link #intersection(List)}.
	 * </p>
	 *
	 * @param segments the array of segments to intersect
	 * @return a new {@link Segment} representing the intersection
	 * @throws IllegalArgumentException if {@code segments} is empty, if segments
	 *                                  have different directions, or if no
	 *                                  overlapping range exists
	 * @throws NullPointerException     if {@code segments} is {@code null}
	 * @see #intersection(List)
	 */
	public static Segment intersection(Segment[] segments) {
		Objects.requireNonNull(segments, "segments must not be null");

		return intersection(Arrays.asList(segments));
	}

	/**
	 * Computes the intersection of multiple segments with a custom value function.
	 *
	 * <p>
	 * Convenience overload for array inputs. Delegates to
	 * {@link #intersection(List, BiFunction)}.
	 * </p>
	 *
	 * @param segments      the array of segments to intersect
	 * @param valueFunction a function that combines two {@link Fraction} values
	 * @return a new {@link Segment} representing the intersection
	 * @throws IllegalArgumentException if {@code segments} is empty, if segments
	 *                                  have different directions, or if no
	 *                                  overlapping range exists
	 * @throws NullPointerException     if {@code segments} or {@code valueFunction}
	 *                                  is {@code null}
	 * @see #intersection(List, BiFunction)
	 */
	public static Segment intersection(Segment[] segments, BiFunction<Fraction, Fraction, Fraction> valueFunction) {
		Objects.requireNonNull(segments, "segments must not be null");

		return intersection(Arrays.asList(segments), valueFunction);
	}

	public LocalDateTime computeClampedStart(LocalDateTime current) {
		if (direction == TimeDirection.FORWARD)
			return current.isAfter(startExclusive()) ? current : startExclusive();
		else
			return current.isBefore(startExclusive()) ? current : startExclusive();
	}

}