/** * Obtains a {@code PeriodFields} from an array of single-unit periods. * * <p>The period fields must all have different units. * * @param periods the array of single-unit periods, not null * @return the {@code PeriodFields} instance, not null * @throws IllegalArgumentException if the same period unit occurs twice */ public static PeriodFields of(PeriodField... periods) { checkNotNull(periods, "PeriodField array must not be null"); TreeMap<PeriodUnit, PeriodField> internalMap = createMap(); for (PeriodField period : periods) { checkNotNull(period, "PeriodField array must not contain null"); if (internalMap.put(period.getUnit(), period) != null) { throw new IllegalArgumentException("PeriodField array contains the same unit twice"); } } return create(internalMap); }
/** * Returns a copy of this period with only those units that can be converted to the specified * units. * * <p>This method will return a new period where every field can be converted to one of the * specified units. In the result, each of the retained periods will have the same amount as they * do in this period - no conversion or normalization occurs. * * <p>For example, if this period is '2 Days, 5 Hours, 7 Minutes' and the specified unit array * contains 'Seconds' then the output will be '5 Hours, 7 Minutes'. The 'Days' unit is not * retained as it cannot be converted to 'Seconds'. * * <p>This instance is immutable and unaffected by this method call. * * @param units the units to retain, not altered, not null, no nulls * @return a {@code PeriodFields} based on this period with the specified units retained, not null */ public PeriodFields retainConvertible(PeriodUnit... units) { checkNotNull(units, "PeriodUnit array must not be null"); TreeMap<PeriodUnit, PeriodField> copy = clonedMap(); outer: for (Iterator<PeriodUnit> it = copy.keySet().iterator(); it.hasNext(); ) { PeriodUnit loopUnit = it.next(); for (PeriodUnit unit : units) { checkNotNull(unit, "PeriodUnit array must not contain null"); if (loopUnit.isConvertibleTo(unit)) { continue outer; } } it.remove(); } return create(copy); }
/** * Obtains a {@code PeriodFields} from a {@code Duration} based on the standard durations of * seconds and nanoseconds. * * <p>The conversion will create an instance with two units - the {@code ISOChronology} seconds * and nanoseconds units. This matches the {@link #toDuration()} method. * * @param duration the duration to create from, not null * @return the {@code PeriodFields} instance, not null */ public static PeriodFields of(Duration duration) { checkNotNull(duration, "Duration must not be null"); TreeMap<PeriodUnit, PeriodField> internalMap = createMap(); internalMap.put(SECONDS, PeriodField.of(duration.getSeconds(), SECONDS)); internalMap.put(NANOS, PeriodField.of(duration.getNanoOfSecond(), NANOS)); return create(internalMap); }
/** * Obtains a {@code PeriodFields} from an amount and unit, by extending any fractional remainder * onto smaller units. * * <p>The parameters represent the two parts of a phrase like 'one-and-a-half Hours'. The * fractional parts will be distributed into the smaller units, and rounded down when no smaller * units exist. If the {@code fractionalAmount} is negative, the amount of the biggest unit will * be negative, the rest will be positive. * * @param fractionalAmount the amount of create with, positive or negative * @param unit the period unit, not null * @return the {@code PeriodFields} instance, not null */ public static PeriodFields of(double fractionalAmount, PeriodUnit unit) { checkNotNull(unit, "PeriodUnit must not be null"); PeriodUnit currentUnit = unit; double fudge = 0.000000000000001d; TreeMap<PeriodUnit, PeriodField> internalMap = createMap(); do { long floor = (long) Math.floor(fractionalAmount + fudge); if (floor != 0) { internalMap.put(currentUnit, PeriodField.of(floor, currentUnit)); } double remainder = fractionalAmount - floor; // will be positive PeriodField nextEquivalent = currentUnit.getNextEquivalentPeriod(); if (nextEquivalent != null) { currentUnit = nextEquivalent.getUnit(); fractionalAmount = remainder * nextEquivalent.getAmount(); fudge *= nextEquivalent.getAmount(); } else { // No smaller units exist, we're done currentUnit = null; } } while (currentUnit != null && Math.abs(fractionalAmount) > fudge); if (internalMap.isEmpty()) { return of(0L, unit); } else { return create(internalMap); } }
/** * Totals this period in terms of a single unit. * * <p>This will take each of the stored {@code PeriodField} instances and convert them to the * specified unit. The result will be the total of these converted periods. * * <p>For example, '3 Hours, 34 Minutes' can be totalled to minutes resulting in '214 Minutes'. * * @param unit the unit to total in, not null * @return a period equivalent to the total of this period in a single unit, not null * @throws CalendricalException if this period cannot be converted to the unit * @throws ArithmeticException if the calculation overflows */ public PeriodField toTotal(PeriodUnit unit) { checkNotNull(unit, "PeriodUnit must not be null"); PeriodField result = null; for (PeriodField period : unitFieldMap.values()) { period = period.toEquivalent(unit); result = (result != null ? result.plus(period) : period); } return result; }
/** * Returns a copy of this period with the specified unit removed. * * <p>If this period already contains an amount for the unit then the amount is removed. * Otherwise, no action occurs. * * <p>This instance is immutable and unaffected by this method call. * * @param unit the unit to remove, not null * @return a {@code PeriodFields} based on this period with the specified unit removed, not null */ public PeriodFields without(PeriodUnit unit) { checkNotNull(unit, "PeriodUnit must not be null"); if (unitFieldMap.containsKey(unit) == false) { return this; } TreeMap<PeriodUnit, PeriodField> copy = clonedMap(); copy.remove(unit); return create(copy); }
/** * Returns a copy of this period with the specified units retained. * * <p>This method will return a new period that only has the specified units. All units not * present in the input will not be present in the result. In most cases, the result will not be * equivalent to this period. * * <p>This instance is immutable and unaffected by this method call. * * @param units the units to retain, not altered, not null, no nulls * @return a {@code PeriodFields} based on this period with the specified units retained, not null */ public PeriodFields retain(PeriodUnit... units) { checkNotNull(units, "PeriodUnit array must not be null"); TreeMap<PeriodUnit, PeriodField> copy = clonedMap(); List<PeriodUnit> unitList = Arrays.asList(units); if (unitList.contains(null)) { throw new NullPointerException("PeriodUnit array must not contain null"); } copy.keySet().retainAll(unitList); return create(copy); }
/** * Returns a copy of this period with the specified period subtracted. * * <p>The result will contain the units and amounts from this period minus the specified unit and * amount. The specified unit will always be in the result even if the amount is zero. * * <p>This instance is immutable and unaffected by this method call. * * @param amount the amount to subtract, measured in the specified unit, positive or negative * @param unit the unit defining the amount, not null * @return a {@code PeriodFields} based on this period with the specified period subtracted, not * null * @throws ArithmeticException if the calculation overflows */ public PeriodFields minus(long amount, PeriodUnit unit) { checkNotNull(unit, "PeiodRule must not be null"); if (amount == 0 && contains(unit)) { return this; } TreeMap<PeriodUnit, PeriodField> copy = clonedMap(); PeriodField old = copy.get(unit); copy.put(unit, old != null ? old.minus(amount) : PeriodField.of(amount, unit).negated()); return create(copy); }
/** * Converts this period to one containing only the units specified. * * <p>This converts this period to one measured in the specified units. It operates by looping * through the individual parts of this period, converting each in turn to one of the specified * units. These converted periods are then combined to form the result. * * <p>No normalization is performed on the result. This means that an amount in a smaller unit * cannot be converted to an amount in a larger unit. If you need to do this, call {@link * #normalized()} before calling this method. * * <p>This method uses {@link PeriodField#toEquivalent(PeriodUnit...)} and as such, it is * recommended to specify the units from largest to smallest. * * <p>For example, '3 Hours' can normally be converted to both minutes and seconds. If the units * array contains both 'Minutes' and 'Seconds', then the result will be measured in whichever is * first in the array. * * @param units the required unit array, not altered, not null, no nulls * @return a period equivalent to this period, not null * @throws CalendricalException if this period cannot be converted to any of the units * @throws ArithmeticException if the calculation overflows */ public PeriodFields toEquivalent(PeriodUnit... units) { checkNotNull(units, "PeriodUnit array must not be null"); TreeMap<PeriodUnit, PeriodField> map = createMap(); for (PeriodField period : unitFieldMap.values()) { period = period.toEquivalent(units); PeriodField old = map.get(period.getUnit()); period = (old != null ? old.plus(period) : period); map.put(period.getUnit(), period); } return (map.equals(unitFieldMap) ? this : create(map)); }
/** * Returns a copy of this period with the amounts normalized to the specified units. * * <p>This will normalize the period around the specified units. The calculation examines each * pair of units that have a fixed conversion factor. Each pair is adjusted so that the amount in * the smaller unit does not exceed the amount of the fixed conversion factor. At least one unit * must be specified for this method to have any effect. * * <p>For example, a period of '2 Decades, 2 Years, 17 Months' normalized using 'Years' and * 'Months' will return '23 Years, 5 Months'. * * <p>Any part of this period that cannot be converted to one of the specified units will be * unaffected in the result. * * <p>The result will always contain all the specified units, even if they are zero. The result * will be equivalent to this period. * * @param units the unit array to normalize to, not altered, not null, no nulls * @return a period equivalent to this period with the amounts normalized, not null * @throws ArithmeticException if the calculation overflows */ public PeriodFields normalizedTo(PeriodUnit... units) { checkNotNull(units, "PeriodUnit array must not be null"); PeriodFields result = this; TreeSet<PeriodUnit> targetUnits = new TreeSet<PeriodUnit>(Collections.reverseOrder()); targetUnits.addAll(Arrays.asList(units)); // normalize any fields in this period that have a unit greater than the // largest unit in the target set that can be normalized // eg. normalize Years-Months when the target set only contains Months for (PeriodUnit loopUnit : unitFieldMap.keySet()) { for (PeriodUnit targetUnit : targetUnits) { if (targetUnits.contains(loopUnit) == false) { PeriodField conversion = loopUnit.getEquivalentPeriod(targetUnit); if (conversion != null) { long amount = result.getAmount(loopUnit); result = result.plus(conversion.multipliedBy(amount)).without(loopUnit); break; } } } } // algorithm works by finding pairs to check // the first rule is to avoid numeric overflow wherever possible, such as when // Seconds and Minutes are both MAX_VALUE - // eg. the Hour-Minute and Hour-Second pair must be processed before the Minute-Second pair // the second rule is to handle the case where processing two pairs causes a knock on // effect on a pair that has already been processed according to the first rule - // eg. when the Hour-Minute pair is 59 and the Minute-Second pair is 61 // this is achieved by restarting the whole algorithm (the process loop) for (boolean process = true; process; ) { process = false; for (PeriodUnit targetUnit : targetUnits) { for (PeriodUnit loopUnit : result.unitFieldMap.keySet()) { if (targetUnit.equals(loopUnit) == false) { PeriodField conversion = targetUnit.getEquivalentPeriod(loopUnit); if (conversion != null) { long convertAmount = conversion.getAmount(); long amount = result.getAmount(loopUnit); if (amount >= convertAmount || amount <= -convertAmount) { result = result .with(amount % convertAmount, loopUnit) .plus(amount / convertAmount, targetUnit); process = (units.length > 2); // need to re-check from start } } } } result = result.plus(0, targetUnit); // ensure unit is in the result } } return result; }
/** * Obtains a {@code PeriodFields} by totalling the amounts in a list of {@code PeriodProvider} * instances. * * <p>This method returns a period with all the unit-amount pairs from the providers totalled. * Thus a period of '2 Months and 5 Days' combined with a period of '7 Days and 21 Hours' will * yield a result of '2 Months, 12 Days and 21 Hours'. * * @param periodProviders the providers to total, not null * @return the {@code PeriodFields} instance, not null * @throws NullPointerException if any period provider is null or returns null */ public static PeriodFields ofTotal(PeriodProvider... periodProviders) { checkNotNull(periodProviders, "PeriodProvider[] must not be null"); if (periodProviders.length == 1) { return of(periodProviders[0]); } TreeMap<PeriodUnit, PeriodField> map = createMap(); for (PeriodProvider periodProvider : periodProviders) { PeriodFields periods = of(periodProvider); for (PeriodField period : periods.unitFieldMap.values()) { PeriodField old = map.get(period.getUnit()); period = (old != null ? old.plus(period) : period); map.put(period.getUnit(), period); } } return create(map); }
/** * Returns a copy of this period with the modular division remainder of each field calculated with * respect to the specified period. * * <p>This method will return a new period where every field represents a period less than the * specified period. If this period contains a period that cannot be converted to the specified * unit then an exception is thrown. * * <p>For example, if this period is '37 Hours, 7 Minutes' and the specified period is '24 Hours' * then the output will be '13 Hours, 7 Minutes'. * * <p>This method requires this period to be convertible to the specified period. To ensure this * is true, call {@link #retainConvertible}, with the base unit of the period passed into this * method, before calling this method. * * <p>This instance is immutable and unaffected by this method call. * * @param period the period to calculate the remainder against, not null * @return a {@code PeriodFields} based on this period with the remainder, not null * @throws CalendricalException if any field cannot be converted to the unit of the period */ public PeriodFields remainder(PeriodField period) { checkNotNull(period, "PeriodField must not be null"); TreeMap<PeriodUnit, PeriodField> copy = createMap(); for (PeriodField loopField : unitFieldMap.values()) { if (loopField.getUnit().equals(period.getUnit())) { copy.put(loopField.getUnit(), loopField.remainder(period.getAmount())); } else { for (PeriodField equivalent : period.getUnit().getEquivalentPeriods()) { if (loopField.getUnit().equals(equivalent.getUnit())) { copy.put(loopField.getUnit(), loopField.remainder(equivalent.getAmount())); } } } } if (copy.size() < size()) { throw new CalendricalException( "Unable to calculate remainder as some fields cannot be converted"); } return create(copy); }
/** * Obtains a {@code PeriodFields} from an amount and unit. * * <p>The parameters represent the two parts of a phrase like '6 Days'. * * @param amount the amount of create with, positive or negative * @param unit the period unit, not null * @return the {@code PeriodFields} instance, not null */ public static PeriodFields of(long amount, PeriodUnit unit) { checkNotNull(unit, "PeriodUnit must not be null"); TreeMap<PeriodUnit, PeriodField> internalMap = createMap(); internalMap.put(unit, PeriodField.of(amount, unit)); return create(internalMap); }
/** * Gets the period for the specified unit. * * <p>This method allows the period to be queried by unit, like a map. If the unit is not found * then {@code null} is returned. * * @param unit the unit to query, not null * @return the period, null if no period stored for the unit */ public PeriodField get(PeriodUnit unit) { checkNotNull(unit, "PeriodUnit must not be null"); return unitFieldMap.get(unit); }
/** * Obtains a {@code PeriodFields} from a {@code PeriodProvider}. * * <p>This method provides null-checking around {@link PeriodProvider#toPeriodFields()}. * * @param periodProvider the provider to create from, not null * @return the {@code PeriodFields} instance, not null * @throws NullPointerException if the period provider is null or returns null */ public static PeriodFields of(PeriodProvider periodProvider) { checkNotNull(periodProvider, "PeriodProvider must not be null"); PeriodFields result = periodProvider.toPeriodFields(); checkNotNull(result, "PeriodProvider implementation must not return null"); return result; }
/** * Obtains a {@code PeriodFields} from a single-unit period. * * @param period the single-unit period, not null * @return the {@code PeriodFields} instance, not null */ public static PeriodFields of(PeriodField period) { checkNotNull(period, "PeriodField must not be null"); TreeMap<PeriodUnit, PeriodField> internalMap = createMap(); internalMap.put(period.getUnit(), period); return create(internalMap); }