class AvsAnRule extends Rule {

  enum Determiner {
    A, AN, A_OR_AN, UNKNOWN
  }

  private static final Pattern cleanupPattern = compile("[^αa-zA-Z0-9.;,:']");
  private static final Pattern delimPattern = compile("[-\"“'‘()\\[\\]]+");
  private static final Pattern dashQuotePattern = compile("[-']");

  public AvsAnRule(ResourceBundle messages) {
    super.setCategory(Categories.MISC.getCategory(messages));
    setLocQualityIssueType(ITSIssueType.Misspelling);
    setUrl(Tools.getUrl("https://languagetool.org/insights/post/indefinite-articles/"));
    addExamplePair(Example.wrong("The train arrived <marker>a hour</marker> ago."),
                   Example.fixed("The train arrived <marker>an hour</marker> ago."));
  }

  @Override
  public String getId() {
    return "EN_A_VS_AN";
  }

  @Override
  public String getDescription() {
    return "Use of 'a' vs. 'an'";
  }

  @Override
  public int estimateContextForSureMatch() {
    return 1;
  }

  @Override
  public RuleMatch[] match(AnalyzedSentence sentence) {<FILL_FUNCTION_BODY>}

  /**
   * Adds "a" or "an" to the English noun. Used for suggesting the proper form of the indefinite article.
   * For the rare cases where both "a" and "an" are considered okay (e.g. for "historical"), "a" is returned.
   * @param origWord Word that needs an article.
   * @return String containing the word with a determiner, or just the word if the word is an abbreviation.
   */
  public String suggestAorAn(String origWord) {
    AnalyzedTokenReadings token = new AnalyzedTokenReadings(new AnalyzedToken(origWord, null, null), 0);
    Determiner determiner = getCorrectDeterminerFor(token);
    if (determiner == Determiner.A || determiner == Determiner.A_OR_AN) {
      return "a " + StringTools.lowercaseFirstCharIfCapitalized(origWord);
    } else if (determiner == Determiner.AN) {
      return "an " + StringTools.lowercaseFirstCharIfCapitalized(origWord);
    } else {
      return origWord;
    }
  }

  static Determiner getCorrectDeterminerFor(AnalyzedTokenReadings token) {
    String word = token.getToken();
    Determiner determiner = Determiner.UNKNOWN;
    String[] parts = dashQuotePattern.split(word);  // for example, in "one-way" only "one" is relevant
    if (parts.length >= 1 && !parts[0].equalsIgnoreCase("a")) {  // avoid false alarm on "A-levels are..."
      word = parts[0];
    }
    if (token.isWhitespaceBefore() || !"-".equals(word)) { // e.g., 'a- or anti- are prefixes'
      word = cleanupPattern.matcher(word).replaceAll("");         // e.g. >>an "industry party"<<
      if (StringTools.isEmpty(word)) {
        return Determiner.UNKNOWN;
      }
    }
    if (getWordsRequiringA().contains(word.toLowerCase()) || getWordsRequiringA().contains(word)) {
      determiner = Determiner.A;
    }
    if (getWordsRequiringAn().contains(word.toLowerCase()) || getWordsRequiringAn().contains(word)) {
      if (determiner == Determiner.A) {
        determiner = Determiner.A_OR_AN;   // e.g. for 'historical'
      } else {
        determiner = Determiner.AN;
      }
    }
    if (determiner == Determiner.UNKNOWN) {
      char tokenFirstChar = word.charAt(0);
      if (StringTools.isAllUppercase(word) || StringTools.isMixedCase(word)) {
        // we don't know how all-uppercase words (often abbreviations) are pronounced,
        // so never complain about these
        determiner = Determiner.UNKNOWN;
      } else if (isVowel(tokenFirstChar)) {
        determiner = Determiner.AN;
      } else {
        determiner = Determiner.A;
      }
    }
    return determiner;
  }

  private static boolean isVowel(char c) {
    char lc = Character.toLowerCase(c);
    return lc == 'a' || lc == 'e' || lc == 'i' || lc == 'o' || lc == 'u';
  }

}

    List<RuleMatch> ruleMatches = new ArrayList<>();
    AnalyzedTokenReadings[] tokens = sentence.getTokensWithoutWhitespace();
    int prevTokenIndex = 0;
    boolean isSentenceStart;
    boolean equalsA;
    boolean equalsAn;
    for (int i = 1; i < tokens.length; i++) {  // ignoring token 0, i.e., SENT_START
      AnalyzedTokenReadings token = tokens[i];
      String prevTokenStr = prevTokenIndex > 0 ? tokens[prevTokenIndex].getToken() : null;
      isSentenceStart = prevTokenIndex == 1;
      if (!isSentenceStart) {
        equalsA = "a".equals(prevTokenStr);
        equalsAn = "an".equals(prevTokenStr);
      } else {
      	equalsA = "a".equalsIgnoreCase(prevTokenStr);
        equalsAn = "an".equalsIgnoreCase(prevTokenStr);
      }
      if (equalsA || equalsAn) {
        Determiner determiner = getCorrectDeterminerFor(token);
        String msg = null;
        if (equalsA && determiner == Determiner.AN) {
          String replacement = StringTools.startsWithUppercase(prevTokenStr) ? "An" : "an";
          msg = "Use <suggestion>" + replacement + "</suggestion> instead of '" + prevTokenStr + "' if the following "+
                  "word starts with a vowel sound, e.g. 'an article', 'an hour'.";
        } else if (equalsAn && determiner == Determiner.A) {
          String replacement = StringTools.startsWithUppercase(prevTokenStr) ? "A" : "a";
          msg = "Use <suggestion>" + replacement + "</suggestion> instead of '" + prevTokenStr + "' if the following "+
                  "word doesn't start with a vowel sound, e.g. 'a sentence', 'a university'.";
        }
        if (msg != null) {
          RuleMatch match = new RuleMatch(
              this, sentence, tokens[prevTokenIndex].getStartPos(), tokens[prevTokenIndex].getEndPos(),
                  tokens[prevTokenIndex].getStartPos(), token.getEndPos(), msg, "Wrong article");
          ruleMatches.add(match);
        }
      }
      String nextToken = "";
      if (i + 1 < tokens.length) {
        nextToken = tokens[i + 1].getToken();
      }
      if (token.hasPosTag("DT")) {
        prevTokenIndex = i;
      } else if (nextToken.length() > 1 && delimPattern.matcher(token.getToken()).matches()) {
        // skip e.g. the quote in >>an "industry party"<<
      } else {
        prevTokenIndex = 0;
      }
    }
    return toRuleMatchArray(ruleMatches);
  
<methods>public void <init>() ,public void <init>(java.util.ResourceBundle) ,public void addTags(List<java.lang.String>) ,public void addToneTags(List<java.lang.String>) ,public int estimateContextForSureMatch() ,public List<org.languagetool.tagging.disambiguation.rules.DisambiguationPatternRule> getAntiPatterns() ,public org.languagetool.rules.Category getCategory() ,public java.lang.String getConfigureText() ,public final List<org.languagetool.rules.CorrectExample> getCorrectExamples() ,public int getDefaultValue() ,public abstract java.lang.String getDescription() ,public int getDistanceTokens() ,public final List<org.languagetool.rules.ErrorTriggeringExample> getErrorTriggeringExamples() ,public java.lang.String getFullId() ,public abstract java.lang.String getId() ,public final List<org.languagetool.rules.IncorrectExample> getIncorrectExamples() ,public org.languagetool.rules.ITSIssueType getLocQualityIssueType() ,public int getMaxConfigurableValue() ,public int getMinConfigurableValue() ,public int getMinPrevMatches() ,public int getPriority() ,public java.lang.String getSourceFile() ,public java.lang.String getSubId() ,public List<org.languagetool.Tag> getTags() ,public List<org.languagetool.ToneTag> getToneTags() ,public java.net.URL getUrl() ,public boolean hasConfigurableValue() ,public boolean hasTag(org.languagetool.Tag) ,public boolean hasToneTag(org.languagetool.ToneTag) ,public final boolean isDefaultOff() ,public final boolean isDefaultTempOff() ,public boolean isDictionaryBasedSpellingRule() ,public boolean isGoalSpecific() ,public final boolean isOfficeDefaultOff() ,public final boolean isOfficeDefaultOn() ,public boolean isPremium() ,public abstract org.languagetool.rules.RuleMatch[] match(org.languagetool.AnalyzedSentence) throws java.io.IOException,public final void setCategory(org.languagetool.rules.Category) ,public final void setCorrectExamples(List<org.languagetool.rules.CorrectExample>) ,public final void setDefaultOff() ,public final void setDefaultOn() ,public final void setDefaultTempOff() ,public void setDistanceTokens(int) ,public final void setErrorTriggeringExamples(List<org.languagetool.rules.ErrorTriggeringExample>) ,public void setGoalSpecific(boolean) ,public final void setIncorrectExamples(List<org.languagetool.rules.IncorrectExample>) ,public void setLocQualityIssueType(org.languagetool.rules.ITSIssueType) ,public void setMinPrevMatches(int) ,public final void setOfficeDefaultOff() ,public final void setOfficeDefaultOn() ,public void setPremium(boolean) ,public void setPriority(int) ,public void setTags(List<org.languagetool.Tag>) ,public void setToneTags(List<org.languagetool.ToneTag>) ,public void setUrl(java.net.URL) ,public boolean supportsLanguage(org.languagetool.Language) ,public boolean useInOffice() <variables>private static final org.languagetool.rules.Category MISC,private org.languagetool.rules.Category category,private List<org.languagetool.rules.CorrectExample> correctExamples,private boolean defaultOff,private boolean defaultTempOff,private int distanceTokens,private List<org.languagetool.rules.ErrorTriggeringExample> errorTriggeringExamples,private List<org.languagetool.rules.IncorrectExample> incorrectExamples,private boolean isGoalSpecific,private boolean isPremium,private org.languagetool.rules.ITSIssueType locQualityIssueType,protected final non-sealed java.util.ResourceBundle messages,private int minPrevMatches,private boolean officeDefaultOff,private boolean officeDefaultOn,private int priority,private List<org.languagetool.Tag> tags,private List<org.languagetool.ToneTag> toneTags,private java.net.URL url