Home Reference Source

packages/extra/src/Temporals.js

/**
 * @copyright (c) 2015-present, Philipp Thürwächter & Pattrick Hüper & Michał Sobkiewicz
 * @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 * @license BSD-3-Clause (see LICENSE in the root directory of this source tree)
 */

import { requireNonNull } from './assert';
import { ChronoField, ChronoUnit, DateTimeException, DateTimeParseException, IsoFields, ParsePosition, TemporalAdjuster, UnsupportedTemporalTypeException } from '@js-joda/core';

import { _ as jodaInternal } from '@js-joda/core';

const MathUtil = jodaInternal.MathUtil;

/**
 * Additional utilities for working with temporal classes.
 * 
 * This includes:
 * - adjusters that ignore Saturday/Sunday weekends
 * - conversion between `TimeUnit` and `ChronoUnit`
 * - converting an amount to another unit
 *
 */
export class Temporals {
    //-------------------------------------------------------------------------
    /**
     * Returns an adjuster that returns the next working day, ignoring Saturday and Sunday.
     * 
     * Some territories have weekends that do not consist of Saturday and Sunday.
     * No implementation is supplied to support this, however an adjuster
     * can be easily written to do so.
     *
     * @return {TemporalAdjuster} the next working day adjuster, not null
     */
    static nextWorkingDay() {
        return Adjuster.NEXT_WORKING;
    }

    /**
     * Returns an adjuster that returns the next working day or same day if already working day, ignoring Saturday and Sunday.
     * 
     * Some territories have weekends that do not consist of Saturday and Sunday.
     * No implementation is supplied to support this, however an adjuster
     * can be easily written to do so.
     * 
     * @return {TemporalAdjuster} the next working day or same adjuster, not null
     */
    static nextWorkingDayOrSame() {
        return Adjuster.NEXT_WORKING_OR_SAME;
    }

    /**
     * Returns an adjuster that returns the previous working day, ignoring Saturday and Sunday.
     * 
     * Some territories have weekends that do not consist of Saturday and Sunday.
     * No implementation is supplied to support this, however an adjuster
     * can be easily written to do so.
     *
     * @return {TemporalAdjuster} the previous working day adjuster, not null
     */
    static previousWorkingDay() {
        return Adjuster.PREVIOUS_WORKING;
    }

    /**
     * Returns an adjuster that returns the previous working day or same day if already working day, ignoring Saturday and Sunday.
     * 
     * Some territories have weekends that do not consist of Saturday and Sunday.
     * No implementation is supplied to support this, however an adjuster
     * can be easily written to do so.
     * 
     * @return {TemporalAdjuster} the previous working day or same adjuster, not null
     */
    static previousWorkingDayOrSame() {
        return Adjuster.PREVIOUS_WORKING_OR_SAME;
    }

    //-------------------------------------------------------------------------
    /**
     * Parses the text using one of the formatters.
     * 
     * This will try each formatter in turn, attempting to fully parse the specified text.
     * The temporal query is typically a method reference to a `from(TemporalAccessor)` method.
     * For example:
     * ```
     *  LocalDateTime dt = Temporals.parseFirstMatching(str, LocalDateTime.FROM, fm1, fm2, fm3);
     * ```
     * If the parse completes without reading the entire length of the text,
     * or a problem occurs during parsing or merging, then an exception is thrown.
     *
     * @param {string} text  the text to parse, not null
     * @param {TemporalQuery} query  the query defining the type to parse to, not null
     * @param {DateTimeFormatter[]} formatters  the formatters to try, not null
     * @return {*} the parsed date-time, not null
     * @throws {DateTimeParseException} if unable to parse the requested result
     */
    static parseFirstMatching(text, query, ...formatters) {
        requireNonNull(text, 'text');
        requireNonNull(query, 'query');
        requireNonNull(formatters, 'formatters');
        if (formatters.length === 0) {
            throw new DateTimeParseException('No formatters specified', text, 0);
        }
        if (formatters.length === 1) {
            return formatters[0].parse(text, query);
        }
        for (const formatter of formatters) {
            try {
                const pp = new ParsePosition(0);
                formatter.parseUnresolved(text, pp);
                const len = text.length;
                if (pp.getErrorIndex() === -1 && pp.getIndex() === len) {
                    return formatter.parse(text, query);
                }
            } catch (ignored) {
                // should not happen, but ignore if it does
            }
        }
        throw new DateTimeParseException(`Text '${text}' could not be parsed`, text, 0);
    }

    //-------------------------------------------------------------------------
    /**
     * Converts an amount from one unit to another.
     * 
     * This works on the units in `ChronoUnit` and `IsoFields`.
     * The `DAYS` and `WEEKS` units are handled as exact multiple of 24 hours.
     * The `ERAS` and `FOREVER` units are not supported.
     *
     * @param {long} amount  the input amount in terms of the `fromUnit`
     * @param {TemporalUnit} fromUnit  the unit to convert from, not null
     * @param {TemporalUnit} toUnit  the unit to convert to, not null
     * @return {long[]} the conversion array,
     *  element 0 is the signed whole number,
     *  element 1 is the signed remainder in terms of the input unit,
     *  not null
     * @throws DateTimeException if the units cannot be converted
     * @throws UnsupportedTemporalTypeException if the units are not supported
     * @throws ArithmeticException if numeric overflow occurs
     */
    static convertAmount(amount, fromUnit, toUnit) {
        requireNonNull(fromUnit, 'fromUnit');
        requireNonNull(toUnit, 'toUnit');
        Temporals._validateUnit(fromUnit);
        Temporals._validateUnit(toUnit);
        if (fromUnit === toUnit) {
            return [amount, 0];
        }
        // precise-based
        if (Temporals._isPrecise(fromUnit) && Temporals._isPrecise(toUnit)) {
            const fromNanos = fromUnit.duration().toNanos();
            const toNanos = toUnit.duration().toNanos();
            if (fromNanos > toNanos) {
                const multiple = MathUtil.intDiv(fromNanos, toNanos);
                return [MathUtil.safeMultiply(amount, multiple), 0];
            } else {
                const multiple = MathUtil.intDiv(toNanos, fromNanos);
                return [MathUtil.intDiv(amount, multiple), MathUtil.intMod(amount, multiple)];
            }
        }
        // month-based
        const fromMonthFactor = Temporals._monthMonthFactor(fromUnit, fromUnit, toUnit);
        const toMonthFactor = Temporals._monthMonthFactor(toUnit, fromUnit, toUnit);
        if (fromMonthFactor > toMonthFactor) {
            const multiple = MathUtil.intDiv(fromMonthFactor, toMonthFactor);
            return [MathUtil.safeMultiply(amount, multiple), 0];
        } else {
            const multiple = MathUtil.intDiv(toMonthFactor, fromMonthFactor);
            return [MathUtil.intDiv(amount, multiple), MathUtil.intMod(amount, multiple)];
        }
    }

    /**
     * 
     * @param {TemporalUnit} unit
     * @private
     */
    static _validateUnit(unit) {
        if (unit instanceof ChronoUnit) {
            if (unit === ChronoUnit.ERAS || unit === ChronoUnit.FOREVER) {
                throw new UnsupportedTemporalTypeException(`Unsupported TemporalUnit: ${unit}`);
            }
        } else if (unit !== IsoFields.QUARTER_YEARS) {
            throw new UnsupportedTemporalTypeException(`Unsupported TemporalUnit: ${unit}`);
        }
    }

    /**
     * 
     * @param {TemporalUnit} unit
     * @return {boolean}
     * @private
     */
    static _isPrecise(unit) {
        return unit instanceof ChronoUnit && unit.compareTo(ChronoUnit.WEEKS) <= 0;
    }

    /**
     * 
     * @param {TemporalUnit} unit
     * @param {TemporalUnit} fromUnit
     * @param {TemporalUnit} toUnit
     * @return {int}
     * @private
     */
    static _monthMonthFactor(unit, fromUnit, toUnit) {
        if (unit instanceof ChronoUnit) {
            switch (unit) {
                case ChronoUnit.MONTHS:
                    return 1;
                case ChronoUnit.YEARS:
                    return 12;
                case ChronoUnit.DECADES:
                    return 120;
                case ChronoUnit.CENTURIES:
                    return 1200;
                case ChronoUnit.MILLENNIA:
                    return 12000;
                default:
                    throw new DateTimeException(`Unable to convert between units: ${fromUnit} to ${toUnit}`);
            }
        }
        return 3; // quarters
    }

    /**
     * Restricted constructor.
     * 
     * @private
     */
    constructor() {
    }
}

const Adjuster = {};

export function _init() {
    /** Next working day adjuster. */
    Adjuster.NEXT_WORKING = new class extends TemporalAdjuster {
        adjustInto(temporal) {
            const dow = temporal.get(ChronoField.DAY_OF_WEEK);
            switch (dow) {
                case 6:  // Saturday
                    return temporal.plus(2, ChronoUnit.DAYS);
                case 5:  // Friday
                    return temporal.plus(3, ChronoUnit.DAYS);
                default:
                    return temporal.plus(1, ChronoUnit.DAYS);
            }
        }
    };

    /** Previous working day adjuster. */
    Adjuster.PREVIOUS_WORKING = new class extends TemporalAdjuster {
        adjustInto(temporal) {
            const dow = temporal.get(ChronoField.DAY_OF_WEEK);
            switch (dow) {
                case 1:  // Monday
                    return temporal.minus(3, ChronoUnit.DAYS);
                case 7:  // Sunday
                    return temporal.minus(2, ChronoUnit.DAYS);
                default:
                    return temporal.minus(1, ChronoUnit.DAYS);
            }
        }
    };

    /** Next working day or same adjuster. */
    Adjuster.NEXT_WORKING_OR_SAME = new class extends TemporalAdjuster {
        adjustInto(temporal) {
            const dow = temporal.get(ChronoField.DAY_OF_WEEK);
            switch (dow) {
                case 6: // Saturday
                    return temporal.plus(2, ChronoUnit.DAYS);
                case 7: // Sunday
                    return temporal.plus(1, ChronoUnit.DAYS);
                default:
                    return temporal;
            }
        }
    };

    /** Previous working day or same adjuster. */
    Adjuster.PREVIOUS_WORKING_OR_SAME = new class extends TemporalAdjuster {
        adjustInto(temporal) {
            const dow = temporal.get(ChronoField.DAY_OF_WEEK);
            switch (dow) {
                case 6: //Saturday
                    return temporal.minus(1, ChronoUnit.DAYS);
                case 7:  // Sunday
                    return temporal.minus(2, ChronoUnit.DAYS);
                default:
                    return temporal;
            }
        }
    };
}