All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.setl.json.primitive.numbers.NumberParser Maven / Gradle / Ivy

Go to download

An implementation of the Canonical JSON format with support for javax.json and Jackson

The newest version!
package io.setl.json.primitive.numbers;

import static io.setl.json.parser.Parser.isWhite;
import static io.setl.json.parser.Parser.safe;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import jakarta.json.stream.JsonParsingException;

import io.setl.json.io.Input;
import io.setl.json.primitive.cache.CacheManager;
import io.setl.json.primitive.cache.ICache;

/**
 * Parser for JSON numeric values.
 *
 * @author Simon Greatrix on 13/01/2020.
 */
public class NumberParser {

  /** Integer wrapper types. These classes are final so cannot be sub-classed. */
  private static final Set> INTEGER_TYPES = Set.of(
      Byte.class, Short.class, Integer.class, Long.class);



  private enum Step {
    /** Processing the starting character. */
    START(false) {
      Step apply(int r, NumberParser p) {
        if (r == '-') {
          return LEADING_MINUS;
        }
        if (r == '0') {
          return LEADING_ZERO;
        }
        return INTEGER_PART;
      }
    },

    /** Seen leading minus sign, must be followed by digit. */
    LEADING_MINUS(false) {
      Step apply(int r, NumberParser p) {
        if (r == '0') {
          return LEADING_ZERO;
        }
        if (isDigit(r)) {
          return INTEGER_PART;
        }
        return ERROR;
      }
    },

    /** Parsing step failed. */
    ERROR(true) {
      Step apply(int r, NumberParser p) {
        throw new IllegalStateException("Step invoked after error");
      }
    },

    /**
     * Seen leading zero, must be followed by '.', 'e', or 'E'.
     * Note that JSON does not allow multiple redundant leading zeroes.
     */
    LEADING_ZERO(true) {
      Step apply(int r, NumberParser p) {
        if (r == '.') {
          // start fractional part
          return START_FRACTION;
        } else if (r == 'e' || r == 'E') {
          // start exponent part
          return START_EXPONENT;
        }
        return ERROR;
      }
    },

    /** Seen leading 1 to 9, can be followed by any digit, '.', 'e', or 'E'. */
    INTEGER_PART(true) {
      Step apply(int r, NumberParser p) {
        if (isDigit(r)) {
          return INTEGER_PART;
        }
        if (r == '.') {
          return START_FRACTION;
        }
        if (r == 'e' || r == 'E') {
          return START_EXPONENT;
        }
        return ERROR;
      }
    },

    /** Seen a '.', so read fractional part. */
    START_FRACTION(false) {
      Step apply(int r, NumberParser p) {
        p.needBigDecimal = true;
        if (isDigit(r)) {
          return FRACTION_PART;
        }
        return ERROR;
      }
    },

    /** Seen a '.' and a digit, now reading fractional part. */
    FRACTION_PART(true) {
      Step apply(int r, NumberParser p) {
        if (isDigit(r)) {
          return FRACTION_PART;
        }
        if (r == 'e' || r == 'E') {
          return START_EXPONENT;
        }
        return ERROR;
      }
    },

    /** seen 'e' or 'E', read exponent part. Can be '+', '-' or '0' to '9' */
    START_EXPONENT(false) {
      Step apply(int r, NumberParser p) {
        p.needBigDecimal = true;
        if (r == '+' || r == '-') {
          return SIGNED_EXPONENT;
        }
        if (isDigit(r)) {
          return EXPONENT_PART;
        }
        return ERROR;
      }
    },

    /** Seen "E+" or "E-". A digit is required. */
    SIGNED_EXPONENT(false) {
      Step apply(int r, NumberParser p) {
        return isDigit(r) ? EXPONENT_PART : ERROR;
      }
    },

    EXPONENT_PART(true) {
      Step apply(int r, NumberParser p) {
        return isDigit(r) ? EXPONENT_PART : ERROR;
      }
    };

    private final boolean terminal;


    Step(boolean terminal) {
      this.terminal = terminal;
    }


    /**
     * Apply the character and advance to the next step in parsing.
     *
     * @param r the character
     *
     * @return the next step
     */
    abstract Step apply(int r, NumberParser parser);


    /** Can parsing legally stop in this state?. */
    boolean isFinal() {
      return terminal;
    }
  }


  private static JsonParsingException badNumber(StringBuilder buf, int r, Input input) {
    return new JsonParsingException(
        "Invalid character in JSON number: \"" + buf.toString() + "\" was followed by " + safe(r),
        input.getLocation()
    );
  }


  /**
   * Create a JSON number from a string with an appropriate backing type.
   *
   * @param txt the string
   *
   * @return the JSON number
   */
  protected static CJNumber doCreate(String txt) {
    int length = txt.length();

    // Integer.MAX_VALUE takes 10 characters
    if (length < 10) {
      return CJNumber.create(Integer.parseInt(txt));
    }

    // Integer.MIN_VALUE takes 11 characters, so this one could be a long or an int.
    if (length < 12) {
      return doCreateLargeInt(txt);
    }

    // Long.MAX_VALUE takes 19 characters
    if (length < 19) {
      return new CJLong(Long.parseLong(txt));
    }

    // Long.MIN_VALUE takes 20 characters
    if (length < 21) {
      return doCreateLargeLong(txt);
    }

    // Could be a big integer, unless it has too many trailing zeros.
    int s = 0;
    for (int i = txt.length() - 1; i >= 0; i--) {
      if (txt.charAt(i) == '0') {
        s--;
      } else {
        break;
      }
    }

    if (s < CJBigInteger.MIN_SCALE) {
      return new CJBigDecimal(new BigDecimal(txt));
    }

    // It is a BigInteger after all
    return new CJBigInteger(new BigInteger(txt));
  }


  /**
   * Create a JSON number from a string.
   *
   * @param txt the string
   *
   * @return the JSON number
   */
  protected static CJNumber doCreateBigDecimal(String txt) {
    return CJNumber.cast(new BigDecimal(txt));
  }


  /** Create a JSON number from a string with 10 or 11 characters and hence could be a long or a large int. */
  private static CJNumber doCreateLargeInt(String txt) {
    long l = Long.parseLong(txt);
    if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) {
      return CJNumber.create((int) l);
    }
    return new CJLong(l);
  }


  /** Create a JSON number from a string with 19 to 21 characters and hence could be a long or a BigInteger. */
  private static CJNumber doCreateLargeLong(String txt) {
    BigInteger bi = new BigInteger(txt);
    if (bi.bitLength() <= 63) {
      return new CJLong(bi.longValueExact());
    }
    return new CJBigInteger(bi);
  }


  private static boolean isDigit(int r) {
    return '0' <= r && r <= '9';
  }


  private static boolean isEndChar(int r) {
    return r == ',' || r == ']' || r == '}';
  }


  /**
   * Test if the number's type is something whose value can be safely represented by a long, int, short or byte.
   *
   * @param n the number to test
   *
   * @return true if the number's type is an integer of 64 bits ore less.
   */
  public static boolean isPrimitiveIntegerType(Number n) {
    if (n == null) {
      return false;
    }
    if (INTEGER_TYPES.contains(n.getClass())) {
      return true;
    }
    return n instanceof AtomicInteger || n instanceof AtomicLong;
  }


  final Input input;

  boolean needBigDecimal;


  /**
   * New instance.
   *
   * @param input the input to read from
   */
  public NumberParser(Input input) {
    this.input = input;
  }


  /**
   * Does the character end the number?. The actual character is the first one that is not part of the number and will need to be unread for later processing.
   *
   * @param r the character
   *
   * @return true if no longer in the number
   */
  private boolean isEnd(int r, Step step) {
    if (r == -1 || isWhite(r) || isEndChar(r)) {
      input.unread(r);
      // Check for an invalid final state
      if (step.isFinal()) {
        return true;
      }

      // invalid final state
      throw new JsonParsingException("Incomplete JSON number", input.getLocation());
    }

    // not an ending character
    return false;
  }


  /**
   * Parse a number from the input.
   *
   * @param r the initial character of the number
   *
   * @return the parsed number
   *
   * @throws JsonParsingException if the input is invalid
   */
  public CJNumber parse(int r) {
    StringBuilder buf = new StringBuilder();
    buf.append((char) r);

    // read rest of number
    Step step = Step.START.apply(r, this);
    while (true) {
      r = input.read();
      if (isEnd(r, step)) {
        break;
      }
      buf.append((char) r);
      step = step.apply(r, this);
      if (step == Step.ERROR) {
        throw badNumber(buf, r, input);
      }
    }

    String txt = buf.toString();

    ICache cache = CacheManager.numberCache();
    CJNumber pNumber;
    try {
      if (needBigDecimal) {
        pNumber = cache.get(txt, NumberParser::doCreateBigDecimal);
      } else {
        pNumber = cache.get(txt, NumberParser::doCreate);
      }
    } catch (NumberFormatException | ArithmeticException e) {
      pNumber = new BadNumber(new JsonParsingException("Invalid number", e, input.getLocation()));
    }
    pNumber.check();
    return pNumber;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy