/** * 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); } }
/** * 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; }
/** * 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); }
private Period parsePeriod(XMLDocument xmlDoc, String checkDataQueryPath) throws XPathExpressionException { final String periodValue = parseTextValue(xmlDoc, checkDataQueryPath + CHKDATA_DOMAIN_PERIOD_EXPR); final String unitValue = parseTextValue(xmlDoc, checkDataQueryPath + CHKDATA_DOMAIN_PERIOD_UNIT_EXPR); return (periodValue != null) ? new Period(PeriodUnit.value(unitValue), Integer.parseInt(periodValue)) : null; }