/**
   * Note - caller should reset {@link #_pointer} upon <code>null</code> result
   *
   * @return The sheet name as an identifier <code>null</code> if '!' is not found in the right
   *     place
   */
  private SheetIdentifier parseSheetName() {

    String bookName;
    if (look == '[') {
      StringBuilder sb = new StringBuilder();
      GetChar();
      while (look != ']') {
        sb.append(look);
        GetChar();
      }
      GetChar();
      bookName = sb.toString();
    } else {
      bookName = null;
    }

    if (look == '\'') {
      StringBuffer sb = new StringBuffer();

      Match('\'');
      boolean done = look == '\'';
      while (!done) {
        sb.append(look);
        GetChar();
        if (look == '\'') {
          Match('\'');
          done = look != '\'';
        }
      }

      Identifier iden = new Identifier(sb.toString(), true);
      // quoted identifier - can't concatenate anything more
      SkipWhite();
      if (look == '!') {
        GetChar();
        return new SheetIdentifier(bookName, iden, _book);
      }
      return null;
    }

    // unquoted sheet names must start with underscore or a letter
    if (look == '_' || Character.isLetter(look)) {
      StringBuilder sb = new StringBuilder();
      // can concatenate idens with dots
      while (isUnquotedSheetNameChar(look)) {
        sb.append(look);
        GetChar();
      }
      SkipWhite();
      if (look == '!') {
        GetChar();
        return new SheetIdentifier(bookName, new Identifier(sb.toString(), false), _book);
      }
      return null;
    }
    return null;
  }
  /**
   * Parses simple factors that are not primitive ranges or range components i.e. '!', ':'(and equiv
   * '...') do not appear Examples
   *
   * <pre>
   *   my.named...range.
   *   foo.bar(123.456, "abc")
   *   123.456
   *   "abc"
   *   true
   * </pre>
   */
  private ParseNode parseNonRange(int savePointer) {
    resetPointer(savePointer);

    if (Character.isDigit(look)) {
      return new ParseNode(parseNumber());
    }
    if (look == '"') {
      return new ParseNode(new StringPtg(parseStringLiteral()));
    }
    // from now on we can only be dealing with non-quoted identifiers
    // which will either be named ranges or functions
    StringBuilder sb = new StringBuilder();

    // defined names may begin with a letter or underscore
    if (!Character.isLetter(look) && look != '_') {
      throw expected("number, string, or defined name");
    }
    while (isValidDefinedNameChar(look)) {
      sb.append(look);
      GetChar();
    }
    SkipWhite();
    String name = sb.toString();
    if (look == '(') {
      return function(name);
    }
    if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) {
      return new ParseNode(BoolPtg.valueOf(name.equalsIgnoreCase("TRUE")));
    }
    if (_book == null) {
      // Only test cases omit the book (expecting it not to be needed)
      throw new IllegalStateException("Need book to evaluate name '" + name + "'");
    }
    // 20101115, [email protected]: shall provide a temporary defined named record
    // EvaluationName evalName = _book.getName(name, _sheetIndex);
    EvaluationName evalName = _book.getOrCreateName(name, _sheetIndex);
    if (evalName == null) {
      // 20101112, [email protected]: shall return #NAME? error
      // throw new FormulaParseException("Specified named range '"
      //		+ name + "' does not exist in the current workbook.");
      return new ParseNode(ErrPtg.NAME_INVALID);
    }
    // 20101115, [email protected]: unnecessary check
    // if (evalName.isRange()) {
    //	return new ParseNode(evalName.createPtg());
    // }
    // TODO - what about NameX ?
    // throw new FormulaParseException("Specified name '"
    //		+ name + "' is not a range as expected.");
    return new ParseNode(evalName.createPtg());
  }
  /**
   * Parses out a potential LHS or RHS of a ':' intended to produce a plain AreaRef. Normally these
   * are proper cell references but they could also be row or column refs like "$AC" or "10"
   *
   * @return <code>null</code> (and leaves {@link #_pointer} unchanged if a proper range part does
   *     not parse out
   */
  private SimpleRangePart parseSimpleRangePart() {
    int ptr = _pointer - 1; // TODO avoid StringIndexOutOfBounds
    boolean hasDigits = false;
    boolean hasLetters = false;
    while (ptr < _formulaLength) {
      char ch = _formulaString.charAt(ptr);
      if (Character.isDigit(ch)) {
        hasDigits = true;
      } else if (Character.isLetter(ch)) {
        hasLetters = true;
      } else if (ch == '$' || ch == '_') {
        //
      } else {
        break;
      }
      ptr++;
    }
    if (ptr <= _pointer - 1) {
      return null;
    }
    String rep = _formulaString.substring(_pointer - 1, ptr);
    if (!CELL_REF_PATTERN.matcher(rep).matches()) {
      return null;
    }
    // Check range bounds against grid max
    if (hasLetters && hasDigits) {
      if (!isValidCellReference(rep)) {
        return null;
      }
    } else if (hasLetters) {
      if (!CellReference.isColumnWithnRange(rep.replace("$", ""), _ssVersion)) {
        return null;
      }
    } else if (hasDigits) {
      int i;
      try {
        i = Integer.parseInt(rep.replace("$", ""));
      } catch (NumberFormatException e) {
        return null;
      }
      if (i < 1 || i > 65536) {
        return null;
      }
    } else {
      // just dollars ? can this happen?
      return null;
    }

    resetPointer(ptr + 1); // stepping forward
    return new SimpleRangePart(rep, hasLetters, hasDigits);
  }
 /** Recognize an Alpha Character */
 private static boolean IsAlpha(char c) {
   return Character.isLetter(c) || c == '$' || c == '_';
 }