/** * 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; }
/** * Calculates the accurate duration of this period. * * <p>The conversion is based on the {@code ISOChronology} definition of the seconds and * nanoseconds units. If all the fields in this period can be converted to either seconds or * nanoseconds then the conversion will succeed, subject to calculation overflow. If any field * cannot be converted to these fields above then an exception is thrown. * * @return the duration of this period based on {@code ISOChronology} fields, not null * @throws CalendricalException if this period cannot be converted to an exact duration * @throws ArithmeticException if the calculation overflows */ public Duration toDuration() { PeriodFields period = toEquivalent(SECONDS, NANOS); return Duration.ofSeconds(period.getAmount(SECONDS), period.getAmount(NANOS)); }