diff --git a/includes/batch.inc b/includes/batch.inc
index 7011abfbd52015bd753beb16a940f89e40587df9..727c62560c94063d31ce7882dc87643875061150 100644
--- a/includes/batch.inc
+++ b/includes/batch.inc
@@ -339,6 +339,8 @@ function _batch_process() {
       $progress_message = $old_set['progress_message'];
     }
 
+    // Total progress is the number of operations that have fully run plus the
+    // completion level of the current operation.
     $current    = $total - $remaining + $finished;
     $percentage = _batch_api_percentage($total, $current);
     $elapsed    = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
@@ -373,7 +375,10 @@ function _batch_process() {
  * @param $total
  *   The total number of operations.
  * @param $current
- *   The number of the current operation.
+ *   The number of the current operation. This may be a floating point number
+ *   rather than an integer in the case of a multi-step operation that is not
+ *   yet complete; in that case, the fractional part of $current represents the
+ *   fraction of the operation that has been completed.
  * @return
  *   The properly formatted percentage, as a string. We output percentages
  *   using the correct number of decimal places so that we never print "100%"
@@ -390,7 +395,16 @@ function _batch_api_percentage($total, $current) {
     // We add a new digit at 200, 2000, etc. (since, for example, 199/200
     // would round up to 100% if we didn't).
     $decimal_places = max(0, floor(log10($total / 2.0)) - 1);
-    $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
+    do {
+      // Calculate the percentage to the specified number of decimal places.
+      $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
+      // When $current is an integer, the above calculation will always be
+      // correct. However, if $current is a floating point number (in the case
+      // of a multi-step batch operation that is not yet complete), $percentage
+      // may be erroneously rounded up to 100%. To prevent that, we add one
+      // more decimal place and try again.
+      $decimal_places++;
+    } while ($percentage == '100');
   }
   return $percentage;
 }
diff --git a/modules/simpletest/tests/batch.test b/modules/simpletest/tests/batch.test
index d1c0e0b2fe9aa982d67cad0c07cc573dcf200a6d..f668e52280551d35e22e349b778397b34d7ecc64 100644
--- a/modules/simpletest/tests/batch.test
+++ b/modules/simpletest/tests/batch.test
@@ -365,6 +365,19 @@ class BatchPercentagesUnitTestCase extends DrupalUnitTestCase {
       '99.95' => array('total' => 2000, 'current' => 1999),
       // 19999/20000 should add yet another digit and go to 99.995%.
       '99.995' => array('total' => 20000, 'current' => 19999),
+      // The next five test cases simulate a batch with a single operation
+      // ('total' equals 1) that takes several steps to complete. Within the
+      // operation, we imagine that there are 501 items to process, and 100 are
+      // completed during each step. The percentages we get back should be
+      // rounded the usual way for the first few passes (i.e., 20%, 40%, etc.),
+      // but for the last pass through, when 500 out of 501 items have been
+      // processed, we do not want to round up to 100%, since that would
+      // erroneously indicate that the processing is complete.
+      '20' => array('total' => 1, 'current' => 100/501),
+      '40' => array('total' => 1, 'current' => 200/501),
+      '60' => array('total' => 1, 'current' => 300/501),
+      '80' => array('total' => 1, 'current' => 400/501),
+      '99.8' => array('total' => 1, 'current' => 500/501),
     );
     require_once DRUPAL_ROOT . '/includes/batch.inc';
     parent::setUp();