diff --git a/core/includes/common.inc b/core/includes/common.inc
index 037c5b0..66ce099 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -248,40 +248,29 @@ function check_url($uri) {
  *   A translated string representation of the size.
  */
 function format_size($size, $langcode = NULL) {
-  if ($size < Bytes::KILOBYTE) {
-    return \Drupal::translation()->formatPlural($size, '1 byte', '@count bytes', array(), array('langcode' => $langcode));
-  }
-  else {
-    $size = $size / Bytes::KILOBYTE; // Convert bytes to kilobytes.
-    $units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
-    foreach ($units as $unit) {
-      if (round($size, 2) >= Bytes::KILOBYTE) {
-        $size = $size / Bytes::KILOBYTE;
-      }
-      else {
-        break;
-      }
-    }
-    $args = ['@size' => round($size, 2)];
-    $options = ['langcode' => $langcode];
-    switch ($unit) {
-      case 'KB':
-        return new TranslatableMarkup('@size KB', $args, $options);
-      case 'MB':
-        return new TranslatableMarkup('@size MB', $args, $options);
-      case 'GB':
-        return new TranslatableMarkup('@size GB', $args, $options);
-      case 'TB':
-        return new TranslatableMarkup('@size TB', $args, $options);
-      case 'PB':
-        return new TranslatableMarkup('@size PB', $args, $options);
-      case 'EB':
-        return new TranslatableMarkup('@size EB', $args, $options);
-      case 'ZB':
-        return new TranslatableMarkup('@size ZB', $args, $options);
-      case 'YB':
-        return new TranslatableMarkup('@size YB', $args, $options);
-    }
+  $options = ['langcode' => $langcode];
+  /** @var \Drupal\Component\Utility\BytesUnitAndSize $value */
+  $value = Bytes::toUnitAndSize($size);
+  $args = ['@size' => $value->getSize()];
+  switch ($value->getUnit()) {
+    case 'B':
+      return new PluralTranslatableMarkup($value->getSize(), '1 byte', '@count bytes', [], $options);
+    case 'KB':
+      return new TranslatableMarkup('@size KB', $args, $options);
+    case 'MB':
+      return new TranslatableMarkup('@size MB', $args, $options);
+    case 'GB':
+      return new TranslatableMarkup('@size GB', $args, $options);
+    case 'TB':
+      return new TranslatableMarkup('@size TB', $args, $options);
+    case 'PB':
+      return new TranslatableMarkup('@size PB', $args, $options);
+    case 'EB':
+      return new TranslatableMarkup('@size EB', $args, $options);
+    case 'ZB':
+      return new TranslatableMarkup('@size ZB', $args, $options);
+    case 'YB':
+      return new TranslatableMarkup('@size YB', $args, $options);
   }
 }
 
diff --git a/core/lib/Drupal/Component/Utility/Bytes.php b/core/lib/Drupal/Component/Utility/Bytes.php
index d625b12..a4acda2 100644
--- a/core/lib/Drupal/Component/Utility/Bytes.php
+++ b/core/lib/Drupal/Component/Utility/Bytes.php
@@ -39,4 +39,34 @@ public static function toInt($size) {
     }
   }
 
+  /**
+   * Returns unit of measurement and size of a byte quantity.
+   *
+   * This method returns a BytesUnitAndSize value object containing unit of
+   * measurement and a float representing, rounded to 2 decimals, the size
+   * expressed in the unit of measurement. If you need to get a translation
+   * aware string or object, use format_size().
+   *
+   * @param float $size
+   *   A quantity of bytes.
+   *
+   * @return \Drupal\Component\Utility\BytesUnitAndSize|null
+   *   NULL if the input value is negative, a
+   *   \Drupal\Component\Utility\BytesUnitAndSize value object otherwise.
+   *
+   * @see format_size
+   */
+  public static function toUnitAndSize($size) {
+    if ($size < 0) {
+      return NULL;
+    }
+    foreach (['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] as $unit) {
+      if (round($size, 2) < self::KILOBYTE) {
+        break;
+      }
+      $size = $size / self::KILOBYTE;
+    }
+    return new BytesUnitAndSize(round($size, 2), $unit);
+  }
+
 }
diff --git a/core/lib/Drupal/Component/Utility/BytesUnitAndSize.php b/core/lib/Drupal/Component/Utility/BytesUnitAndSize.php
new file mode 100644
index 0000000..915d30d
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/BytesUnitAndSize.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\Component\Utility;
+
+/**
+ * A value object class to store a bytes quantity with an unit of measurement.
+ */
+class BytesUnitAndSize {
+
+  /**
+   * The bytes quantity.
+   *
+   * @var float
+   */
+  protected $size;
+
+  /**
+   * The unit of measurement of this bytes quantity.
+   *
+   * @var string
+   */
+  protected $unit;
+
+  /**
+   * Constructs a new BytesUnitAndSize object.
+   *
+   * @param int $size
+   *   The bytes quantity.
+   * @param string $unit
+   *   The unit of measurement of this bytes quantity.
+   *
+   * @throws \InvalidArgumentException
+   *   When an invalid parameter is passed in.
+   */
+  public function __construct($size, $unit) {
+    if (!is_numeric($size) || $size < 0) {
+      throw new \InvalidArgumentException("Invalid size ({$size}) specified for a BytesUnitAndSize object");
+    }
+    $this->size = $size;
+    $this->unit = $unit;
+  }
+
+  /**
+   * Gets the bytes quantity.
+   *
+   * @return int
+   *   The bytes quantity.
+   */
+  public function getSize() {
+    return $this->size;
+  }
+
+  /**
+   * Gets the unit of measurement of this bytes quantity.
+   *
+   * @return string
+   *   The unit of measurement of this bytes quantity.
+   */
+  public function getUnit() {
+    return $this->unit;
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php b/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php
index 2bcee75..2db893d 100644
--- a/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php
@@ -19,6 +19,7 @@ protected function setUp() {
     parent::setUp();
     $kb = Bytes::KILOBYTE;
     $this->exactTestCases = array(
+      '0 bytes' => 0,
       '1 byte' => 1,
       '1 KB'   => $kb,
       '1 MB'   => $kb * $kb,
@@ -32,6 +33,7 @@ protected function setUp() {
     $this->roundedTestCases = array(
       '2 bytes' => 2,
       '1 MB' => ($kb * $kb) - 1, // rounded to 1 MB (not 1000 or 1024 kilobyte!)
+      '3.77 GB' => 4053371676, // not 3.78 GB as before #2157945
       round(3623651 / ($this->exactTestCases['1 MB']), 2) . ' MB' => 3623651, // megabytes
       round(67234178751368124 / ($this->exactTestCases['1 PB']), 2) . ' PB' => 67234178751368124, // petabytes
       round(235346823821125814962843827 / ($this->exactTestCases['1 YB']), 2) . ' YB' => 235346823821125814962843827, // yottabytes
diff --git a/core/tests/Drupal/Tests/Component/Utility/BytesTest.php b/core/tests/Drupal/Tests/Component/Utility/BytesTest.php
index f4ebf0e..eeeccbc 100644
--- a/core/tests/Drupal/Tests/Component/Utility/BytesTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/BytesTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\Component\Utility;
 
 use Drupal\Component\Utility\Bytes;
+use Drupal\Component\Utility\BytesUnitAndSize;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -57,4 +58,49 @@ public function providerTestToInt() {
     );
   }
 
+  /**
+   * Tests \Drupal\Component\Utility\Bytes::toUnitAndSize().
+   *
+   * @param int $size
+   *   The value for the size argument for
+   *   \Drupal\Component\Utility\Bytes::toUnitAndSize().
+   * @param \Drupal\Component\Utility\BytesUnitAndSize $expected_unit_and_size
+   *   The expected return value from
+   *   \Drupal\Component\Utility\Bytes::toUnitAndSize().
+   *
+   * @dataProvider providerTestToUnitAndSize
+   * @covers ::toUnitAndSize
+   */
+  public function testToUnitAndSize($size, BytesUnitAndSize $expected_unit_and_size = NULL) {
+    $this->assertEquals($expected_unit_and_size, Bytes::toUnitAndSize($size));
+  }
+
+  /**
+   * Provides data for ::testToUnitAndSize.
+   *
+   * @return array[]
+   *   An array of arrays, each containing the argument for
+   *   \Drupal\Component\Utility\Bytes::toUnitAndSize(): size and the expected
+   *   return value.
+   */
+  public function providerTestToUnitAndSize() {
+    return [
+      [0, new BytesUnitAndSize(0, 'B')],
+      [1, new BytesUnitAndSize(1, 'B')],
+      [-1, NULL],
+      [768, new BytesUnitAndSize(768, 'B')],
+      [Bytes::KILOBYTE, new BytesUnitAndSize(1, 'KB')],
+      [pow(Bytes::KILOBYTE, 2), new BytesUnitAndSize(1, 'MB')],
+      [pow(Bytes::KILOBYTE, 3), new BytesUnitAndSize(1, 'GB')],
+      [pow(Bytes::KILOBYTE, 4), new BytesUnitAndSize(1, 'TB')],
+      [pow(Bytes::KILOBYTE, 5), new BytesUnitAndSize(1, 'PB')],
+      [pow(Bytes::KILOBYTE, 6), new BytesUnitAndSize(1, 'EB')],
+      [pow(Bytes::KILOBYTE, 7), new BytesUnitAndSize(1, 'ZB')],
+      [pow(Bytes::KILOBYTE, 8), new BytesUnitAndSize(1, 'YB')],
+      [23476892, new BytesUnitAndSize(22.39, 'MB')],
+      [79691776, new BytesUnitAndSize(76, 'MB')],
+      [81862076662, new BytesUnitAndSize(76.24, 'GB')],
+      [4053371676, new BytesUnitAndSize(3.77, 'GB')],
+    ];
+  }
 }
