Combiner.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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

/**
 * Utilities to combine multiple {@link PiecewiseConstant} functions.
 *
 * <p>
 * In the New Gantt Model context, {@link PiecewiseConstant} typically represents
 * a time-dependent workload allocation expressed as a {@link Fraction}.
 * </p>
 */
public class Combiner {
	/**
	 * Combines several workload functions by summing their values.
	 *
	 * <p>
	 * This operation is meant to model multiple resources assigned to the same task.
	 * For example, if two people are each allocated at 1/2 on a given day, the
	 * resulting combined workload for that day is 1 (i.e., one full-time equivalent).
	 * </p>
	 *
	 * @param functions the workload functions to add
	 * @return a new {@link PiecewiseConstant} representing the summed allocation
	 */
	public static PiecewiseConstant sum(PiecewiseConstant... functions) {
		throw new UnsupportedOperationException("Work in progress");
	}

	/**
	 * Combines several functions by multiplying their values.
	 *
	 * <p>
	 * This operation is particularly useful to combine an "assignment" with
	 * an "availability calendar".
	 * </p>
	 *
	 * <p>
	 * Availability calendars typically use only {@code 0} and {@code 1} to indicate
	 * closed/open periods. Multiplying a workload allocation by such a calendar
	 * acts as a logical AND:
	 * </p>
	 *
	 * <ul>
	 *   <li>{@code 1 * allocation = allocation} (open period)</li>
	 *   <li>{@code 0 * allocation = 0} (closed period)</li>
	 * </ul>
	 *
	 * <p>
	 * This allows you to automatically "mask" allocations during non-working times.
	 * </p>
	 *
	 * <p>
	 * It also allows combining calendars with each other. When multiple
	 * availability calendars are multiplied, the result represents the
	 * intersection of their opening periods: the combined calendar is open
	 * only when all input calendars are open.
	 * </p>
	 *
	 * <p>
	 * For example, you may have one calendar for weekday business days,
	 * another for public holidays, and another for daily working hours.
	 * Multiplying them yields a single calendar that is open only when all
	 * these constraints are simultaneously satisfied.
	 * </p>
	 *
	 * @param functions the functions to multiply
	 * @return a new {@link PiecewiseConstant} representing the combined result
	 */
	public static PiecewiseConstant product(PiecewiseConstant... functions) {
		return CombinedPiecewiseConstant.of(Fraction.PRODUCT)
				.with(functions);
	}
	
	
	/**
	 * A {@link PiecewiseConstant} that combines multiple functions using a specified
	 * operation.
	 *
	 * <p>
	 * This class allows flexible combination of workload functions by applying
	 * a user-defined operation (e.g., sum, product) to their values at each instant.
	 * </p>
	 */
	public static class CombinedPiecewiseConstant extends AbstractPiecewiseConstant {
		
		/** The functions to combine. */
		private final List<PiecewiseConstant> functions;
		
		/** The operation used to combine function values. */
		private final BiFunction<Fraction, Fraction, Fraction> valueCombiner;
		
		/** 
		 * Constructs a CombinedPiecewiseConstant with the specified functions and combiner.
		 * 
		 * @param functions the functions to combine
		 * @param valueCombiner the operation to combine function values
		 */
		private CombinedPiecewiseConstant(List<PiecewiseConstant> functions, BiFunction<Fraction, Fraction, Fraction> valueCombiner) {
			Objects.requireNonNull(functions, "functions must not be null");
			Objects.requireNonNull(valueCombiner, "valueCombiner must not be null");
			
			this.functions = functions;
			this.valueCombiner = valueCombiner;
		}
		
		/** 
		 * Constructs an empty CombinedPiecewiseConstant with the specified combiner.
		 * 
		 * @param valueCombiner the operation to combine function values
		 */
		private CombinedPiecewiseConstant(BiFunction<Fraction, Fraction, Fraction> valueCombiner) {
			this.valueCombiner = valueCombiner;
			this.functions = Collections.emptyList();
		}

		
		/**
		 * Retrieves the segment at the specified instant by combining segments
		 * from all constituent functions.
		 * 
		 * @param instant the time instant to query
		 * @return the combined segment at this instant
		 * @throws IllegalStateException if less than two functions are present
		 */
		@Override
		public Segment segmentAt(LocalDateTime instant, TimeDirection direction) {
			if(functions.size() < 2) {
				throw new IllegalStateException("At least two functions are required for combination");
			}
			
			List<Segment> segments = functions.stream()
					.map(f -> f.segmentAt(instant, direction))
					 .collect(Collectors.toList());
			
			return Segment.intersection(segments, valueCombiner);
		}
		
		/**
		 * Creates a new CombinedPiecewiseConstant with the specified value combiner but without any functions.
		 * 
		 * @param valueCombiner the operation to combine function values
		 * @return a new CombinedPiecewiseConstant instance
		 */
		public static CombinedPiecewiseConstant of(BiFunction<Fraction, Fraction, Fraction> valueCombiner) {
			return new CombinedPiecewiseConstant(valueCombiner);
		}
		
		/**
		 * Returns a new CombinedPiecewiseConstant that includes the specified functions.
		 * 
		 * @param functions the functions to add
		 * @return a new CombinedPiecewiseConstant with the added functions
		 */
		public CombinedPiecewiseConstant with(List<PiecewiseConstant> functions) {
			List<PiecewiseConstant> newFunctions = new ArrayList<>(this.functions);
			newFunctions.addAll(functions);
			return new CombinedPiecewiseConstant(newFunctions, valueCombiner);
		}
		
		/**
		 * Returns a new CombinedPiecewiseConstant that includes the specified functions.
		 * 
		 * @param functions the functions to add
		 * @return a new CombinedPiecewiseConstant with the added functions
		 */
		public CombinedPiecewiseConstant with(PiecewiseConstant... functions) {
			return with(Arrays.asList(functions));
		}
	}
}