Home Reference Source

src/format/parser/FractionPrinterParser.js

/**
 * @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper
 * @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 {IllegalArgumentException} from '../../errors';
import {MathUtil} from '../../MathUtil';

/**
 * TODO optimize FractionPrinterParser, fix documentation
 *
 * Prints and parses a numeric date-time field with optional padding.
 * @private
 */
export class FractionPrinterParser {

    /**
     * Constructor.
     *
     * @param {TemporalField} field  the field to output, not null
     * @param {Number} minWidth  the minimum width to output, from 0 to 9
     * @param {Number} maxWidth  the maximum width to output, from 0 to 9
     * @param {boolean} decimalPoint  whether to output the localized decimal point symbol
     */
    constructor(field, minWidth, maxWidth, decimalPoint) {
        requireNonNull(field, 'field');
        if (field.range().isFixed() === false) {
            throw new IllegalArgumentException('Field must have a fixed set of values: ' + field);
        }
        if (minWidth < 0 || minWidth > 9) {
            throw new IllegalArgumentException('Minimum width must be from 0 to 9 inclusive but was ' + minWidth);
        }
        if (maxWidth < 1 || maxWidth > 9) {
            throw new IllegalArgumentException('Maximum width must be from 1 to 9 inclusive but was ' + maxWidth);
        }
        if (maxWidth < minWidth) {
            throw new IllegalArgumentException('Maximum width must exceed or equal the minimum width but ' +
                maxWidth + ' < ' + minWidth);
        }
        this.field = field;
        this.minWidth = minWidth;
        this.maxWidth = maxWidth;
        this.decimalPoint = decimalPoint;
    }

    print(context, buf) {
        const value = context.getValue(this.field);
        if (value === null) {
            return false;
        }
        const symbols = context.symbols();
        if (value === 0) {  // scale is zero if value is zero
            if (this.minWidth > 0) {
                if (this.decimalPoint) {
                    buf.append(symbols.decimalSeparator());
                }
                for (let i = 0; i < this.minWidth; i++) {
                    buf.append(symbols.zeroDigit());
                }
            }
        } else {
            let fraction = this.convertToFraction(value, symbols.zeroDigit());
            const outputScale = Math.min(Math.max(fraction.length, this.minWidth), this.maxWidth);
            fraction = fraction.substr(0, outputScale);
            if(fraction * 1 > 0 ) {
                while (fraction.length > this.minWidth && fraction[fraction.length - 1] === '0') {
                    fraction = fraction.substr(0, fraction.length - 1);
                }
            }
            let str = fraction;
            str = symbols.convertNumberToI18N(str);
            if (this.decimalPoint) {
                buf.append(symbols.decimalSeparator());
            }
            buf.append(str);
        }
        return true;
    }

    parse(context, text, position) {
        const effectiveMin = (context.isStrict() ? this.minWidth : 0);
        const effectiveMax = (context.isStrict() ? this.maxWidth : 9);
        const length = text.length;
        if (position === length) {
            // valid if whole field is optional, invalid if minimum width
            return (effectiveMin > 0 ? ~position : position);
        }
        if (this.decimalPoint) {
            if (text[position] !== context.symbols().decimalSeparator()) {
                // valid if whole field is optional, invalid if minimum width
                return (effectiveMin > 0 ? ~position : position);
            }
            position++;
        }
        const minEndPos = position + effectiveMin;
        if (minEndPos > length) {
            return ~position;  // need at least min width digits
        }
        const maxEndPos = Math.min(position + effectiveMax, length);
        let total = 0;  // can use int because we are only parsing up to 9 digits
        let pos = position;
        while (pos < maxEndPos) {
            const ch = text.charAt(pos++);
            const digit = context.symbols().convertToDigit(ch);
            if (digit < 0) {
                if (pos < minEndPos) {
                    return ~position;  // need at least min width digits
                }
                pos--;
                break;
            }
            total = total * 10 + digit;
        }
        const moveLeft = pos - position;
        const scale = Math.pow(10, moveLeft);
        const value = this.convertFromFraction(total, scale);
        return context.setParsedField(this.field, value, position, pos);
    }

    /**
     *
     * @param {Number} value  the value to convert, must be valid for this rule
     * @param {String} zeroDigit  the character for zero
     * @return {String} the value as a fraction within the range, from 0 to 1, not null
     */
    convertToFraction(value, zeroDigit) {
        const range = this.field.range();
        range.checkValidValue(value, this.field);
        const _min = range.minimum();
        const _range = range.maximum() - _min + 1;
        const _value = value - _min;
        const _scaled = MathUtil.intDiv((_value * 1000000000),  _range);
        let fraction = '' + _scaled;
        while(fraction.length < 9){
            fraction = zeroDigit + fraction;
        }
        return fraction;
    }

    /**
     *
     * @param {Number} total  the fraction to convert, not null
     * @param {Number} scale  the scale, not null
     * @return {Number} the value of the field, valid for this rule
     * @throws DateTimeException if the value cannot be converted
     */
    convertFromFraction(total, scale) {
        const range = this.field.range();
        const _min = range.minimum();
        const _range = range.maximum() - _min + 1;
        const _value = MathUtil.intDiv((total * _range), scale);
        return _value;
    }

    toString() {
        const decimal = (this.decimalPoint ? ',DecimalPoint' : '');
        return 'Fraction(' + this.field + ',' + this.minWidth + ',' + this.maxWidth + decimal + ')';
    }
}