Program: 63410
Project: 63410
Buggy source:
/** 
 * heuristically merge a list of TextElement into a list of TextChunk ported from from PDFBox's PDFTextStripper.writePage, with modifications. Here be dragons
 */
public static List<TextChunk> mergeWords(List<TextElement> textElements,List<Ruling> verticalRulings){
  List<TextChunk> textChunks=new ArrayList<TextChunk>();
  if (textElements.isEmpty()) {
    return textChunks;
  }
  textChunks.add(new TextChunk(textElements.remove(0)));
  TextChunk firstTC=textChunks.get(0);
  float previousAveCharWidth=(float)firstTC.getWidth();
  float endOfLastTextX=(float)firstTC.getRight();
  float maxYForLine=(float)firstTC.getBottom();
  float maxHeightForLine=(float)firstTC.getHeight();
  float minYTopForLine=(float)firstTC.getTop();
  float lastWordSpacing=-1;
  float wordSpacing, deltaSpace, averageCharWidth, deltaCharWidth;
  float expectedStartOfNextWordX, dist;
  TextElement sp, prevChar;
  TextChunk currentChunk;
  boolean sameLine, acrossVerticalRuling;
  for (  TextElement chr : textElements) {
    currentChunk=textChunks.get(textChunks.size() - 1);
    prevChar=currentChunk.textElements.get(currentChunk.textElements.size() - 1);
    if ((chr.getText().equals(prevChar.getText())) && (prevChar.overlapRatio(chr) > 0.5)) {
      continue;
    }
    if (chr.getText().equals("String_Node_Str") && Utils.feq(prevChar.getLeft(),chr.getLeft()) && Utils.feq(prevChar.getTop(),chr.getTop())) {
      continue;
    }
    if ((chr.getFont() != prevChar.getFont()) || !Utils.feq(chr.getFontSize(),prevChar.getFontSize())) {
      previousAveCharWidth=-1;
    }
    acrossVerticalRuling=false;
    for (    Ruling r : verticalRulings) {
      if ((verticallyOverlapsRuling(prevChar,r) && verticallyOverlapsRuling(chr,r)) && (prevChar.x < r.getPosition() && chr.x > r.getPosition()) || (prevChar.x > r.getPosition() && chr.x < r.getPosition())) {
        acrossVerticalRuling=true;
        break;
      }
    }
    wordSpacing=chr.getWidthOfSpace();
    deltaSpace=0;
    if (java.lang.Float.isNaN(wordSpacing) || wordSpacing == 0) {
      deltaSpace=java.lang.Float.MAX_VALUE;
    }
 else     if (lastWordSpacing < 0) {
      deltaSpace=wordSpacing * 0.5f;
    }
 else {
      deltaSpace=((wordSpacing + lastWordSpacing) / 2.0f) * 0.5f;
    }
    if (previousAveCharWidth < 0) {
      averageCharWidth=(float)(chr.getWidth() / chr.getText().length());
    }
 else {
      averageCharWidth=(float)((previousAveCharWidth + (chr.getWidth() / chr.getText().length())) / 2.0f);
    }
    deltaCharWidth=averageCharWidth * AVERAGE_CHAR_TOLERANCE;
    expectedStartOfNextWordX=-java.lang.Float.MAX_VALUE;
    if (endOfLastTextX != -1) {
      expectedStartOfNextWordX=endOfLastTextX + Math.min(deltaCharWidth,deltaSpace);
    }
    sameLine=true;
    if (!Utils.overlap((float)chr.getBottom(),chr.height,maxYForLine,maxHeightForLine)) {
      endOfLastTextX=-1;
      expectedStartOfNextWordX=-java.lang.Float.MAX_VALUE;
      maxYForLine=-java.lang.Float.MAX_VALUE;
      maxHeightForLine=-1;
      minYTopForLine=java.lang.Float.MAX_VALUE;
      sameLine=false;
    }
    endOfLastTextX=(float)chr.getRight();
    if (!acrossVerticalRuling && sameLine && expectedStartOfNextWordX < chr.getLeft() && !prevChar.getText().endsWith("String_Node_Str")) {
      sp=new TextElement((float)prevChar.getTop(),(float)prevChar.getLeft(),(float)(expectedStartOfNextWordX - prevChar.getLeft()),(float)prevChar.getHeight(),prevChar.getFont(),prevChar.getFontSize(),"String_Node_Str",prevChar.getWidthOfSpace());
      currentChunk.add(sp);
    }
 else {
      sp=null;
    }
    maxYForLine=(float)Math.max(chr.getBottom(),maxYForLine);
    maxHeightForLine=(float)Math.max(maxHeightForLine,chr.getHeight());
    minYTopForLine=(float)Math.min(minYTopForLine,chr.getTop());
    dist=(float)(chr.getLeft() - (sp != null ? sp.getRight() : prevChar.getRight()));
    if (!acrossVerticalRuling && sameLine && (dist < 0 ? currentChunk.verticallyOverlaps(chr) : dist < wordSpacing)) {
      currentChunk.add(chr);
    }
 else {
      textChunks.add(new TextChunk(chr));
    }
    lastWordSpacing=wordSpacing;
    previousAveCharWidth=(float)(sp != null ? (averageCharWidth + sp.getWidth()) / 2.0f : averageCharWidth);
  }
  List<TextChunk> textChunksSeparatedByDirectionality=new ArrayList<TextChunk>();
  for (  TextChunk chunk : textChunks) {
    boolean isLtrDominant=chunk.isLtrDominant() != -1;
    TextChunk dirChunk=chunk.groupByDirectionality(isLtrDominant);
    textChunksSeparatedByDirectionality.add(dirChunk);
  }
  return textChunksSeparatedByDirectionality;
}

Fixed version (ground truth):
/** 
 * heuristically merge a list of TextElement into a list of TextChunk ported from from PDFBox's PDFTextStripper.writePage, with modifications. Here be dragons
 */
public static List<TextChunk> mergeWords(List<TextElement> textElements,List<Ruling> verticalRulings){
  List<TextChunk> textChunks=new ArrayList<TextChunk>();
  if (textElements.isEmpty()) {
    return textChunks;
  }
  List<TextElement> copyOfTextElements=new ArrayList<TextElement>(textElements);
  textChunks.add(new TextChunk(copyOfTextElements.remove(0)));
  TextChunk firstTC=textChunks.get(0);
  float previousAveCharWidth=(float)firstTC.getWidth();
  float endOfLastTextX=(float)firstTC.getRight();
  float maxYForLine=(float)firstTC.getBottom();
  float maxHeightForLine=(float)firstTC.getHeight();
  float minYTopForLine=(float)firstTC.getTop();
  float lastWordSpacing=-1;
  float wordSpacing, deltaSpace, averageCharWidth, deltaCharWidth;
  float expectedStartOfNextWordX, dist;
  TextElement sp, prevChar;
  TextChunk currentChunk;
  boolean sameLine, acrossVerticalRuling;
  for (  TextElement chr : copyOfTextElements) {
    currentChunk=textChunks.get(textChunks.size() - 1);
    prevChar=currentChunk.textElements.get(currentChunk.textElements.size() - 1);
    if ((chr.getText().equals(prevChar.getText())) && (prevChar.overlapRatio(chr) > 0.5)) {
      continue;
    }
    if (chr.getText().equals("String_Node_Str") && Utils.feq(prevChar.getLeft(),chr.getLeft()) && Utils.feq(prevChar.getTop(),chr.getTop())) {
      continue;
    }
    if ((chr.getFont() != prevChar.getFont()) || !Utils.feq(chr.getFontSize(),prevChar.getFontSize())) {
      previousAveCharWidth=-1;
    }
    acrossVerticalRuling=false;
    for (    Ruling r : verticalRulings) {
      if ((verticallyOverlapsRuling(prevChar,r) && verticallyOverlapsRuling(chr,r)) && (prevChar.x < r.getPosition() && chr.x > r.getPosition()) || (prevChar.x > r.getPosition() && chr.x < r.getPosition())) {
        acrossVerticalRuling=true;
        break;
      }
    }
    wordSpacing=chr.getWidthOfSpace();
    deltaSpace=0;
    if (java.lang.Float.isNaN(wordSpacing) || wordSpacing == 0) {
      deltaSpace=java.lang.Float.MAX_VALUE;
    }
 else     if (lastWordSpacing < 0) {
      deltaSpace=wordSpacing * 0.5f;
    }
 else {
      deltaSpace=((wordSpacing + lastWordSpacing) / 2.0f) * 0.5f;
    }
    if (previousAveCharWidth < 0) {
      averageCharWidth=(float)(chr.getWidth() / chr.getText().length());
    }
 else {
      averageCharWidth=(float)((previousAveCharWidth + (chr.getWidth() / chr.getText().length())) / 2.0f);
    }
    deltaCharWidth=averageCharWidth * AVERAGE_CHAR_TOLERANCE;
    expectedStartOfNextWordX=-java.lang.Float.MAX_VALUE;
    if (endOfLastTextX != -1) {
      expectedStartOfNextWordX=endOfLastTextX + Math.min(deltaCharWidth,deltaSpace);
    }
    sameLine=true;
    if (!Utils.overlap((float)chr.getBottom(),chr.height,maxYForLine,maxHeightForLine)) {
      endOfLastTextX=-1;
      expectedStartOfNextWordX=-java.lang.Float.MAX_VALUE;
      maxYForLine=-java.lang.Float.MAX_VALUE;
      maxHeightForLine=-1;
      minYTopForLine=java.lang.Float.MAX_VALUE;
      sameLine=false;
    }
    endOfLastTextX=(float)chr.getRight();
    if (!acrossVerticalRuling && sameLine && expectedStartOfNextWordX < chr.getLeft() && !prevChar.getText().endsWith("String_Node_Str")) {
      sp=new TextElement((float)prevChar.getTop(),(float)prevChar.getLeft(),(float)(expectedStartOfNextWordX - prevChar.getLeft()),(float)prevChar.getHeight(),prevChar.getFont(),prevChar.getFontSize(),"String_Node_Str",prevChar.getWidthOfSpace());
      currentChunk.add(sp);
    }
 else {
      sp=null;
    }
    maxYForLine=(float)Math.max(chr.getBottom(),maxYForLine);
    maxHeightForLine=(float)Math.max(maxHeightForLine,chr.getHeight());
    minYTopForLine=(float)Math.min(minYTopForLine,chr.getTop());
    dist=(float)(chr.getLeft() - (sp != null ? sp.getRight() : prevChar.getRight()));
    if (!acrossVerticalRuling && sameLine && (dist < 0 ? currentChunk.verticallyOverlaps(chr) : dist < wordSpacing)) {
      currentChunk.add(chr);
    }
 else {
      textChunks.add(new TextChunk(chr));
    }
    lastWordSpacing=wordSpacing;
    previousAveCharWidth=(float)(sp != null ? (averageCharWidth + sp.getWidth()) / 2.0f : averageCharWidth);
  }
  List<TextChunk> textChunksSeparatedByDirectionality=new ArrayList<TextChunk>();
  for (  TextChunk chunk : textChunks) {
    boolean isLtrDominant=chunk.isLtrDominant() != -1;
    TextChunk dirChunk=chunk.groupByDirectionality(isLtrDominant);
    textChunksSeparatedByDirectionality.add(dirChunk);
  }
  return textChunksSeparatedByDirectionality;
}