diff --git a/src/Interval.php b/src/Interval.php
new file mode 100644
index 0000000..30d1cec
--- /dev/null
+++ b/src/Interval.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Drupal\commerce;
+use Drupal\Core\Datetime\DrupalDateTime;
+
+/**
+ * Provides a value object for intervals (1 month, 14 days, etc).
+ */
+final class Interval {
+
+  /**
+   * The number.
+   *
+   * @var string
+   */
+  protected $number;
+
+  /**
+   * The unit.
+   *
+   * @var string
+   */
+  protected $unit;
+
+  /**
+   * Constructs a new Interval object.
+   *
+   * @param string $number
+   *   The number.
+   * @param string $unit
+   *   The unit.
+   */
+  public function __construct($number, $unit) {
+    if (!is_numeric($number)) {
+      throw new \InvalidArgumentException(sprintf('The provided interval number "%s" is not a numeric value.', $number));
+    }
+    if (!in_array($unit, ['hour', 'day', 'week', 'month', 'year'])) {
+      throw new \InvalidArgumentException(sprintf('Invalid interval unit "%s" provided.', $unit));
+    }
+
+    $this->number = $number;
+    $this->unit = $unit;
+  }
+
+  /**
+   * Gets the number.
+   *
+   * @return string
+   *   The number.
+   */
+  public function getNumber() {
+    return $this->number;
+  }
+
+  /**
+   * Gets the unit.
+   *
+   * @return string
+   *   The unit.
+   */
+  public function getUnit() {
+    return $this->unit;
+  }
+
+  /**
+   * Gets the string representation of the interval.
+   *
+   * @return string
+   *   The string representation of the interval.
+   */
+  public function __toString() {
+    return $this->number . ' ' . $this->unit;
+  }
+
+  /**
+   * Gets the array representation of the interval.
+   *
+   * @return array
+   *   The array representation of the interval.
+   */
+  public function toArray() {
+    return ['number' => $this->number, 'unit' => $this->unit];
+  }
+
+  /**
+   * Adds the interval to the given date.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   The date.
+   *
+   * @return \Drupal\Core\Datetime\DrupalDateTime
+   *   The new date.
+   */
+  public function add(DrupalDateTime $date) {
+    /** @var \DateTime $new_date */
+    $new_date = clone $date;
+    $new_date->modify('+' . $this->__toString());
+    // Jan 31st + 1 month should give Feb 28th, not Mar 3rd.
+    if ($this->unit == 'month' && $new_date->format('d') !== $date->format('d')) {
+      $new_date->modify('last day of previous month');
+    }
+
+    return $new_date;
+  }
+
+  /**
+   * Subtracts the interval from the given date.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   The date.
+   *
+   * @return \Drupal\Core\Datetime\DrupalDateTime
+   *   The new date.
+   */
+  public function subtract(DrupalDateTime $date) {
+    /** @var \DateTime $new_date */
+    $new_date = clone $date;
+    $new_date->modify('-' . $this->__toString());
+    // Mar 31st - 1 month should Feb 28th, not Mar 3rd.
+    if ($this->unit == 'month' && $new_date->format('d') !== $date->format('d')) {
+      $new_date->modify('last day of previous month');
+    }
+
+    return $new_date;
+  }
+
+  /**
+   * Reduces the date to the lower boundary.
+   *
+   * For example, an Apr 14th date would be reduced to Apr 1st for monthly
+   * intervals, and Jan 1st for yearly intervals.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   The date.
+   *
+   * @return \Drupal\Core\Datetime\DrupalDateTime
+   *   The new date.
+   */
+  public function floor(DrupalDateTime $date) {
+    /** @var \DateTime $new_date */
+    $new_date = clone $date;
+    switch ($this->unit) {
+      case 'hour':
+        $new_date->setTime($new_date->format('G'), 0);
+        break;
+
+      case 'day':
+        $new_date->setTime(0, 0, 0);
+        break;
+
+      case 'week':
+        // @todo Account for weeks that start on a sunday.
+        $new_date->modify('monday this week');
+        $new_date->setTime(0, 0, 0);
+        break;
+
+      case 'month':
+        $new_date->modify('first day of this month');
+        $new_date->setTime(0, 0, 0);
+        break;
+
+      case 'year':
+        $new_date->modify('first day of january');
+        $new_date->setTime(0, 0, 0);
+        break;
+    }
+
+    return $new_date;
+  }
+
+  /**
+   * Increases the date to the upper boundary.
+   *
+   * For example, an Apr 14th date would be increased to May 1st for a 1 month
+   * interval, and to June 1st for a 2 month interval.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   The date.
+   *
+   * @return \Drupal\Core\Datetime\DrupalDateTime
+   *   The new date.
+   */
+  public function ceil(DrupalDateTime $date) {
+    return $this->add($this->floor($date));
+  }
+
+}
\ No newline at end of file
diff --git a/tests/src/Kernel/IntervalTest.php b/tests/src/Kernel/IntervalTest.php
new file mode 100644
index 0000000..0f81a90
--- /dev/null
+++ b/tests/src/Kernel/IntervalTest.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace Drupal\Tests\commerce\Kernel;
+
+use Drupal\commerce\Interval;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the Interval class.
+ *
+ * @coversDefaultClass \Drupal\commerce\Interval
+ * @group commerce
+ */
+class IntervalTest extends KernelTestBase {
+
+  /**
+   * The interval.
+   *
+   * @var \Drupal\commerce\Interval
+   */
+  protected $interval;
+
+  /**
+   * Tests creating an interval with an invalid number.
+   *
+   * ::covers __construct.
+   */
+  public function testInvalidNumber() {
+    $this->setExpectedException(\InvalidArgumentException::class);
+    $interval = new Interval('INVALID', 'month');
+  }
+
+  /**
+   * Tests creating an interval with an invalid unit.
+   *
+   * ::covers __construct.
+   */
+  public function testInvalidUnit() {
+    $this->setExpectedException(\InvalidArgumentException::class);
+    $interval = new Interval('1', 'INVALID');
+  }
+
+  /**
+   * Tests the methods for getting the number/unit in various formats.
+   *
+   * ::covers getNumber
+   * ::covers getUnit
+   * ::covers __toString
+   * ::covers toArray.
+   */
+  public function testGetters() {
+    $interval = new Interval('1', 'month');
+    $this->assertEquals('1', $interval->getNumber());
+    $this->assertEquals('month', $interval->getUnit());
+    $this->assertEquals('1 month', $interval->__toString());
+    $this->assertEquals(['number' => '1', 'unit' => 'month'], $interval->toArray());
+  }
+
+  /**
+   * @dataProvider additionDateProvider
+   */
+  public function testAddition($date, Interval $interval, $expected_date) {
+    $date = DrupalDateTime::createFromFormat('Y-m-d H:i', $date);
+    $expected_date = DrupalDateTime::createFromFormat('Y-m-d H:i', $expected_date);
+    $this->assertEquals($expected_date, $interval->add($date));
+  }
+
+  /**
+   * @dataProvider subtractionDateProvider
+   */
+  public function testSubtraction($date, Interval $interval, $expected_date) {
+    $date = DrupalDateTime::createFromFormat('Y-m-d H:i', $date);
+    $expected_date = DrupalDateTime::createFromFormat('Y-m-d H:i', $expected_date);
+    $this->assertEquals($expected_date, $interval->subtract($date));
+  }
+
+  /**
+   * @dataProvider flooringDateProvider
+   */
+  public function testFlooring($date, Interval $interval, $expected_date) {
+    $date = DrupalDateTime::createFromFormat('Y-m-d H:i', $date);
+    $expected_date = DrupalDateTime::createFromFormat('Y-m-d H:i', $expected_date);
+    $this->assertEquals($expected_date, $interval->floor($date));
+  }
+
+  /**
+   * @dataProvider ceilingDateProvider
+   */
+  public function testCeiling($date, Interval $interval, $expected_date) {
+    $date = DrupalDateTime::createFromFormat('Y-m-d H:i', $date);
+    $expected_date = DrupalDateTime::createFromFormat('Y-m-d H:i', $expected_date);
+    $this->assertEquals($expected_date, $interval->ceil($date));
+  }
+
+
+  /**
+   * Data provider for ::testAddition.
+   *
+   * @return array
+   *   A list of testAddition function arguments.
+   */
+  public function additionDateProvider() {
+    return [
+      ['2017-02-24 17:15' , new Interval('1', 'hour'), '2017-02-24 18:15'],
+      ['2017-02-24 17:15' , new Interval('8', 'hour'), '2017-02-25 01:15'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'day'), '2017-02-25 17:15'],
+      ['2017-02-24 17:15' , new Interval('14', 'day'), '2017-03-10 17:15'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'week'), '2017-03-03 17:15'],
+      ['2017-02-24 17:15' , new Interval('3', 'week'), '2017-03-17 17:15'],
+
+      ['2017-02-24 17:15', new Interval('1', 'month'), '2017-03-24 17:15'],
+      ['2017-02-24 17:15', new Interval('2', 'month'), '2017-04-24 17:15'],
+
+      ['2017-01-31 17:15', new Interval('1', 'month'), '2017-02-28 17:15'],
+      ['2017-01-31 17:15', new Interval('3', 'month'), '2017-04-30 17:15'],
+
+      ['2017-02-24 17:15', new Interval('1', 'year'), '2018-02-24 17:15'],
+      ['2017-02-24 17:15', new Interval('2', 'year'), '2019-02-24 17:15'],
+    ];
+  }
+
+  /**
+   * Data provider for ::testSubtraction.
+   *
+   * @return array
+   *   A list of testSubtraction function arguments.
+   */
+  public function subtractionDateProvider() {
+    return [
+      ['2017-02-24 17:15' , new Interval('1', 'hour'), '2017-02-24 16:15'],
+      ['2017-02-24 17:15' , new Interval('18', 'hour'), '2017-02-23 23:15'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'day'), '2017-02-23 17:15'],
+      ['2017-02-24 17:15' , new Interval('30', 'day'), '2017-01-25 17:15'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'week'), '2017-02-17 17:15'],
+      ['2017-02-24 17:15' , new Interval('4', 'week'), '2017-01-27 17:15'],
+
+      ['2017-02-24 17:15', new Interval('1', 'month'), '2017-01-24 17:15'],
+      ['2017-02-24 17:15', new Interval('2', 'month'), '2016-12-24 17:15'],
+
+      ['2017-03-31 17:15', new Interval('1', 'month'), '2017-02-28 17:15'],
+      ['2017-03-31 17:15', new Interval('4', 'month'), '2016-11-30 17:15'],
+
+      ['2017-02-24 17:15', new Interval('1', 'year'), '2016-02-24 17:15'],
+      ['2017-02-24 17:15', new Interval('2', 'year'), '2015-02-24 17:15'],
+    ];
+  }
+
+  /**
+   * Data provider for ::testFlooring.
+   *
+   * @return array
+   *   A list of testFlooring function arguments.
+   */
+  public function flooringDateProvider() {
+    return [
+      ['2017-02-24 17:15' , new Interval('1', 'hour'), '2017-02-24 17:00'],
+      ['2017-02-24 17:15' , new Interval('2', 'hour'), '2017-02-24 17:00'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'day'), '2017-02-24 00:00'],
+      ['2017-02-24 17:15' , new Interval('2', 'day'), '2017-02-24 00:00'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'week'), '2017-02-20 00:00'],
+      ['2017-02-24 17:15' , new Interval('4', 'week'), '2017-02-20 00:00'],
+
+      ['2017-02-24 17:15', new Interval('1', 'month'), '2017-02-01 00:00'],
+      ['2017-02-24 17:15', new Interval('2', 'month'), '2017-02-01 00:00'],
+
+      ['2017-02-24 17:15', new Interval('1', 'year'), '2017-01-01 00:00'],
+      ['2017-02-24 17:15', new Interval('2', 'year'), '2017-01-01 00:00'],
+    ];
+  }
+
+  /**
+   * Data provider for ::testCeiling.
+   *
+   * @return array
+   *   A list of testCeiling function arguments.
+   */
+  public function ceilingDateProvider() {
+    return [
+      ['2017-02-24 17:15' , new Interval('1', 'hour'), '2017-02-24 18:00'],
+      ['2017-02-24 17:15' , new Interval('2', 'hour'), '2017-02-24 19:00'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'day'), '2017-02-25 00:00'],
+      ['2017-02-24 17:15' , new Interval('2', 'day'), '2017-02-26 00:00'],
+
+      ['2017-02-24 17:15' , new Interval('1', 'week'), '2017-02-27 00:00'],
+      ['2017-02-24 17:15' , new Interval('4', 'week'), '2017-03-20 00:00'],
+
+      ['2017-02-24 17:15', new Interval('1', 'month'), '2017-03-01 00:00'],
+      ['2017-02-24 17:15', new Interval('2', 'month'), '2017-04-01 00:00'],
+
+      ['2017-02-24 17:15', new Interval('1', 'year'), '2018-01-01 00:00'],
+      ['2017-02-24 17:15', new Interval('2', 'year'), '2019-01-01 00:00'],
+    ];
+  }
+
+}
