/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.codehaus.groovy.syntax; import antlr.collections.AST; import org.codehaus.groovy.antlr.ASTRuntimeException; import java.math.BigInteger; import java.math.BigDecimal; /** * Helper class for processing Groovy numeric literals. * * @author Brian Larson * @author <a href="mailto:cpoirier@dreaming.org">Chris Poirier</a> */ public class Numbers { //--------------------------------------------------------------------------- // LEXING SUPPORT /** * Returns true if the specified character is a base-10 digit. */ public static boolean isDigit( char c ) { return c >= '0' && c <= '9'; } /** * Returns true if the specific character is a base-8 digit. */ public static boolean isOctalDigit( char c ) { return c >= '0' && c <= '7'; } /** * Returns true if the specified character is a base-16 digit. */ public static boolean isHexDigit( char c ) { return isDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); } /** * Returns true if the specified character is a valid type specifier * for a numeric value. */ public static boolean isNumericTypeSpecifier( char c, boolean isDecimal ) { if( isDecimal ) { switch( c ) { case 'G': case 'g': case 'D': case 'd': case 'F': case 'f': return true; } } else { switch( c ) { case 'G': case 'g': case 'I': case 'i': case 'L': case 'l': return true; } } return false; } //--------------------------------------------------------------------------- // PARSING SUPPORT private static final BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); private static final BigInteger MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE); private static final BigInteger MAX_INTEGER = BigInteger.valueOf(Integer.MAX_VALUE); private static final BigInteger MIN_INTEGER = BigInteger.valueOf(Integer.MIN_VALUE); private static final BigDecimal MAX_DOUBLE = new BigDecimal(String.valueOf(Double.MAX_VALUE)); private static final BigDecimal MIN_DOUBLE = MAX_DOUBLE.negate(); private static final BigDecimal MAX_FLOAT = new BigDecimal(String.valueOf(Float.MAX_VALUE)); private static final BigDecimal MIN_FLOAT = MAX_FLOAT.negate(); /** * Builds a Number from the given integer descriptor. Creates the narrowest * type possible, or a specific type, if specified. * * @param text literal text to parse * @return instantiated Number object * @throws NumberFormatException if the number does not fit within the type * requested by the type specifier suffix (invalid numbers don't make * it here) */ @Deprecated public static Number parseInteger(String text ) { return parseInteger(null, text); } /** * Builds a Number from the given integer descriptor. Creates the narrowest * type possible, or a specific type, if specified. * * @param reportNode at node for error reporting in the parser * @param text literal text to parse * @return instantiated Number object * @throws NumberFormatException if the number does not fit within the type * requested by the type specifier suffix (invalid numbers don't make * it here) */ public static Number parseInteger(AST reportNode, String text ) { // remove place holder underscore before starting text = text.replace("_", ""); char c = ' '; int length = text.length(); // // Strip off the sign, if present boolean negative = false; if( (c = text.charAt(0)) == '-' || c == '+' ) { negative = (c == '-'); text = text.substring( 1, length ); length -= 1; } // // Determine radix (default is 10). int radix = 10; if( text.charAt(0) == '0' && length > 1 ) { c = text.charAt(1); if( c == 'X' || c == 'x' ) { radix = 16; text = text.substring( 2, length); length -= 2; } else if ( c == 'B' || c == 'b' ) { radix = 2; text = text.substring(2, length); length -= 2; } else { radix = 8; } } // // Strip off any type specifier and convert it to lower // case, if present. char type = 'x'; // pick best fit if( isNumericTypeSpecifier(text.charAt(length-1), false) ) { type = Character.toLowerCase( text.charAt(length-1) ); text = text.substring( 0, length-1); length -= 1; } // // Add the sign back, if necessary if( negative ) { text = "-" + text; } // // Build the specified type or, if no type was specified, the // smallest type in which the number will fit. BigInteger value = new BigInteger( text, radix ); switch (type) { case 'i': if (radix==10 && reportNode != null && (value.compareTo(MAX_INTEGER) > 0 || value.compareTo(MIN_INTEGER) < 0) ) { throw new ASTRuntimeException(reportNode, "Number of value "+value+" does not fit in the range of int, but int was enforced."); } else { return Integer.valueOf(value.intValue()); } case 'l': if (radix==10 && reportNode != null && (value.compareTo(MAX_LONG) > 0 || value.compareTo(MIN_LONG) < 0) ) { throw new ASTRuntimeException(reportNode, "Number of value "+value+" does not fit in the range of long, but long was enforced."); } else { return new Long( value.longValue() ); } case 'g': return value ; default: // If not specified, we will return the narrowest possible // of Integer, Long, and BigInteger. if( value.compareTo(MAX_INTEGER) <= 0 && value.compareTo(MIN_INTEGER) >= 0 ) { return Integer.valueOf(value.intValue()); } else if( value.compareTo(MAX_LONG) <= 0 && value.compareTo(MIN_LONG) >= 0 ) { return new Long(value.longValue()); } return value; } } /** * Builds a Number from the given decimal descriptor. Uses BigDecimal, * unless, Double or Float is requested. * * @param text literal text to parse * @return instantiated Number object * @throws NumberFormatException if the number does not fit within the type * requested by the type specifier suffix (invalid numbers don't make * it here) */ public static Number parseDecimal( String text ) { text = text.replace("_", ""); int length = text.length(); // // Strip off any type specifier and convert it to lower // case, if present. char type = 'x'; if( isNumericTypeSpecifier(text.charAt(length-1), true) ) { type = Character.toLowerCase( text.charAt(length-1) ); text = text.substring( 0, length-1 ); length -= 1; } // // Build the specified type or default to BigDecimal BigDecimal value = new BigDecimal( text ); switch( type ) { case 'f': if( value.compareTo(MAX_FLOAT) <= 0 && value.compareTo(MIN_FLOAT) >= 0) { return new Float( text ); } throw new NumberFormatException( "out of range" ); case 'd': if( value.compareTo(MAX_DOUBLE) <= 0 && value.compareTo(MIN_DOUBLE) >= 0) { return new Double( text ); } throw new NumberFormatException( "out of range" ); case 'g': default: return value; } } }