001/*
002 * Units of Measurement Reference Implementation
003 * Copyright (c) 2005-2021, Jean-Marie Dautelle, Werner Keil, Otavio Santana.
004 *
005 * All rights reserved.
006 *
007 * Redistribution and use in source and binary forms, with or without modification,
008 * are permitted provided that the following conditions are met:
009 *
010 * 1. Redistributions of source code must retain the above copyright notice,
011 *    this list of conditions and the following disclaimer.
012 *
013 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions
014 *    and the following disclaimer in the documentation and/or other materials provided with the distribution.
015 *
016 * 3. Neither the name of JSR-385, Indriya nor the names of their contributors may be used to endorse or promote products
017 *    derived from this software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
021 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
023 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
026 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package tech.units.indriya.internal.format;
031
032import static org.apiguardian.api.API.Status.INTERNAL;
033
034import java.math.BigDecimal;
035import java.math.BigInteger;
036import java.text.NumberFormat;
037import java.text.ParseException;
038import java.text.ParsePosition;
039import java.util.Stack;
040
041import javax.measure.format.MeasurementParseException;
042
043import org.apiguardian.api.API;
044
045import tech.units.indriya.format.SimpleQuantityFormat;
046import tech.units.indriya.function.RationalNumber;
047import tech.units.indriya.internal.function.Calculator;
048
049/**
050 * Support class for {@link SimpleQuantityFormat} and {@link RationalNumberFormat}.
051 * <p>
052 * In addition to decimal formats this also parses rational number format {@code 5÷3} or  {@code -5÷3}.
053 * 
054 * @author Andi Huber
055 *
056 */
057@API(status=INTERNAL)
058public class RationalNumberScanner {
059
060    private final CharSequence csq; 
061    private final ParsePosition cursor;
062    private final NumberFormat numberFormat;
063    
064    private boolean divisionCharacterDetected = false;
065    
066    public RationalNumberScanner(CharSequence csq, ParsePosition cursor, NumberFormat numberFormat) {
067        this.csq = csq;
068        this.cursor = cursor;
069        this.numberFormat = numberFormat;
070    }
071    
072    private int scanForStart(int pos) {
073        while (pos < csq.length()) {
074            char c = csq.charAt(pos);
075            if(!Character.isWhitespace(c)) {
076                return pos;
077            }
078            pos++;
079        }
080        return pos;       
081    }
082    
083    private int scanForEnd(int pos) {
084        divisionCharacterDetected = false;
085        while (pos < csq.length()) {
086            char c = csq.charAt(pos);
087            if(c == RationalNumber.DIVISION_CHARACTER) {
088                divisionCharacterDetected = true;
089                break;
090            }
091            if(Character.isWhitespace(c)) {
092                break;
093            } 
094            pos++;
095        }
096        cursor.setIndex(pos + 1);
097        return pos;        
098    }
099
100    public Number getNumber() {
101        
102        Stack<String> numberLiterals = new Stack<>();
103                
104        do {
105            int startDecimal = scanForStart(cursor.getIndex());
106            int endDecimal = scanForEnd(startDecimal+1);
107            
108            String numberLiteral = csq.subSequence(startDecimal, endDecimal).toString();
109            
110            numberLiterals.push(numberLiteral);
111            
112        } while (divisionCharacterDetected);
113        
114        if(numberLiterals.size()==2) {
115            // parsing RationalNumber
116            
117            BigInteger divisor = new BigInteger(numberLiterals.pop());
118            BigInteger dividend = new BigInteger(numberLiterals.pop());
119            return RationalNumber.of(dividend, divisor);
120        }
121
122        if(numberLiterals.size()==1) {
123            // parsing decimal number
124            
125            String numberLiteral = numberLiterals.pop();
126            
127            if(numberFormat==null) {
128                try {
129                    Number bigDecimal = new BigDecimal(numberLiteral);
130                    return Calculator.of(bigDecimal).peek();
131                } catch (Exception e) {
132                    throw new MeasurementParseException("Failed to parse number-literal '"+numberLiteral+"'.");
133                }
134            } 
135            
136            try {
137                return numberFormat.parse(numberLiteral);
138            } catch (ParseException e) {
139                throw new MeasurementParseException(e);
140            }
141            
142        }
143        
144        throw new MeasurementParseException("Unexpected number of number-literals in '" + csq + "'");
145        
146    }
147    
148}