From 653a356b0ad1bdc88f4f7f22244f0bf01b8a617f Mon Sep 17 00:00:00 2001
From: Christian Kolb <info@liplex.de>
Date: Wed, 27 Mar 2024 10:48:59 +0100
Subject: [PATCH] Streamline comparisons in Moment (#40)

Co-authored-by: Christian Kolb <info@digital-craftsman.de>
---
 CHANGELOG.md                                  |   6 +
 README.md                                     |  18 +-
 UPGRADE.md                                    |   4 +
 infection.json5                               |   7 +-
 src/Moment.php                                | 157 +++++++++++++++++-
 src/Time.php                                  |  69 +++-----
 tests/Moment/IsAfterInTimeZoneTest.php        | 119 +++++++++++++
 .../Moment/IsAfterOrEqualToInTimeZoneTest.php | 119 +++++++++++++
 tests/Moment/IsBeforeInTimeZoneTest.php       | 119 +++++++++++++
 .../IsBeforeOrEqualToInTimeZoneTest.php       | 119 +++++++++++++
 tests/Moment/IsEqualToInTimeZoneTest.php      | 119 +++++++++++++
 tests/Moment/IsNotAfterInTimeZoneTest.php     | 119 +++++++++++++
 .../IsNotAfterOrEqualToInTimeZoneTest.php     | 119 +++++++++++++
 tests/Moment/IsNotBeforeInTimeZoneTest.php    | 119 +++++++++++++
 .../IsNotBeforeOrEqualToInTimeZoneTest.php    | 119 +++++++++++++
 tests/Moment/IsNotEqualToInTimeZoneTest.php   | 119 +++++++++++++
 tests/Time/IsNotAfterOrEqualToTest.php        |  71 ++++++++
 tests/Time/IsNotAfterTest.php                 |  71 ++++++++
 tests/Time/IsNotBeforeOrEqualToTest.php       |  71 ++++++++
 tests/Time/IsNotBeforeTest.php                |  71 ++++++++
 20 files changed, 1677 insertions(+), 58 deletions(-)
 create mode 100644 tests/Moment/IsAfterInTimeZoneTest.php
 create mode 100644 tests/Moment/IsAfterOrEqualToInTimeZoneTest.php
 create mode 100644 tests/Moment/IsBeforeInTimeZoneTest.php
 create mode 100644 tests/Moment/IsBeforeOrEqualToInTimeZoneTest.php
 create mode 100644 tests/Moment/IsEqualToInTimeZoneTest.php
 create mode 100644 tests/Moment/IsNotAfterInTimeZoneTest.php
 create mode 100644 tests/Moment/IsNotAfterOrEqualToInTimeZoneTest.php
 create mode 100644 tests/Moment/IsNotBeforeInTimeZoneTest.php
 create mode 100644 tests/Moment/IsNotBeforeOrEqualToInTimeZoneTest.php
 create mode 100644 tests/Moment/IsNotEqualToInTimeZoneTest.php
 create mode 100644 tests/Time/IsNotAfterOrEqualToTest.php
 create mode 100644 tests/Time/IsNotAfterTest.php
 create mode 100644 tests/Time/IsNotBeforeOrEqualToTest.php
 create mode 100644 tests/Time/IsNotBeforeTest.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5defe7d..07442d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## 0.10.0
+
+- Added `is*InTimeZone(Time | Date | Month | Year $equalTo, \DateTimeZone $timeZone): bool` methods to `Moment`.
+- Added missing comparison methods to `Time` (like `isNotAfter(self $time): bool`).
+- Deprecated `isDate*InTimeZone` methods from `Moment` (will be removed in next minor version).
+
 ## 0.9.0
 
 - **[Breaking change](./UPGRADE.md#dropped-support-for-symfony-63)**: Dropped support for Symfony 6.3.
diff --git a/README.md b/README.md
index 92e8180..c6fc59d 100644
--- a/README.md
+++ b/README.md
@@ -36,33 +36,29 @@ Install package through composer:
 composer require digital-craftsman/date-time-precision
 ```
 
-> ⚠️ This bundle can be used (and is being used) in production, but hasn't reached version 1.0 yet. Therefore, there will be breaking changes between minor versions. I'd recommend that you require the bundle only with the current minor version like `composer require digital-craftsman/date-time-precision:0.9.*`. Breaking changes are described in the releases and [the changelog](./CHANGELOG.md). Updates are described in the [upgrade guide](./UPGRADE.md).
+> ⚠️ This bundle can be used (and is being used) in production, but hasn't reached version 1.0 yet. Therefore, there will be breaking changes between minor versions. I'd recommend that you require the bundle only with the current minor version like `composer require digital-craftsman/date-time-precision:0.10.*`. Breaking changes are described in the releases and [the changelog](./CHANGELOG.md). Updates are described in the [upgrade guide](./UPGRADE.md).
 
 ## When would I need that?
 
 Basically whenever you use a `DateTime` object for something other than a single moment.
 
-Storing more information in those cases just lead to more questions, like "When storing the month, do we store the first of month at midnight?" and therefore increases complexity. Additionally, you need mutate or reduce the point in time to be able to compare it. With the package it will be as easy as:
+Storing more information in those cases just lead to more questions, like "When storing the month, do we store the first day of month at midnight, and if so, in which time zone?" and therefore increases complexity. Additionally, you need mutate or reduce the point in time to be able to compare it. With the package it will be as easy as:
 
 ```php
-if ($now
-    ->timeInTimeZone($facilityTimeZone)
-    ->isBefore($facility->openFrom)
-) {
+if ($now->isBeforeInTimeZone($facility->openFrom, $facilityTimeZone)) {
     throw new FacilityIsNotOpenYet();
 }
 ```
+`$now` is a `Moment` (in UTC) and `$facility->openFrom` is a `Time` (in the timezone of the facility).
 
-The idea is that your system and all variables can still remain in the timezone `UTC` and you only call mutations on it when needed for a comparison.
+The idea is that your system can run in `UTC` and all moments are in the timezone `UTC`. But all values that have an implicit time zone like a date or a time of day will be stored with just the data needed. This way we're getting rid of additional data that creates more surface for possible bugs. Through precise value objects and specific comparison functions, the code is more readable than before.
 
 ```php
-if ($now
-    ->dateInTimeZone($facilityTimeZone)
-    ->isBefore($facility->earliestDayOfBooking)
-) {
+if ($now->isBeforeInTimeZone($facility->earliestDayOfBooking)) {
     throw new BookingNotPossibleYet();
 }
 ```
+`$now` is a `Moment` (in UTC) and `$facility->earliestDayOfBooking` is a `Date` (in the timezone of the facility). The same method `isBeforeInTimeZone` that is used previously for the time comparison is the same that is used here. Depending on the type of the second parameter, the comparison is done on the relevant part of the moment.
 
 Modifications work the same way.
 
diff --git a/UPGRADE.md b/UPGRADE.md
index d91b7df..d737226 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -1,5 +1,9 @@
 # Upgrade guide
 
+## From 0.9.* to 0.10.0
+
+No breaking changes (just deprecations).
+
 ## From 0.8.* to 0.9.0
 
 ### Dropped support for Symfony 6.3
diff --git a/infection.json5 b/infection.json5
index 3de20a3..64fc377 100644
--- a/infection.json5
+++ b/infection.json5
@@ -6,7 +6,12 @@
     ]
   },
   "mutators": {
-    "@default": true
+    "@default": true,
+    "InstanceOf_": {
+      "ignore": [
+        "DigitalCraftsman\\DateTimePrecision\\Moment",
+      ]
+    },
   },
   "minMsi": 100,
   "minCoveredMsi": 100
diff --git a/src/Moment.php b/src/Moment.php
index 672f09b..36913e9 100644
--- a/src/Moment.php
+++ b/src/Moment.php
@@ -97,44 +97,153 @@ public function isEqualTo(self $moment): bool
         return $this->dateTime == $moment->dateTime;
     }
 
+    public function isEqualToInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isEqualTo($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isEqualTo($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isEqualTo($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isEqualTo($comparator),
+        };
+    }
+
     public function isNotEqualTo(self $moment): bool
     {
         return $this->dateTime != $moment->dateTime;
     }
 
+    public function isNotEqualToInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isNotEqualTo($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isNotEqualTo($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isNotEqualTo($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isNotEqualTo($comparator),
+        };
+    }
+
     public function isAfter(self $moment): bool
     {
         return $this->dateTime > $moment->dateTime;
     }
 
+    public function isAfterInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isAfter($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isAfter($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isAfter($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isAfter($comparator),
+        };
+    }
+
     public function isNotAfter(self $moment): bool
     {
         return !($this->dateTime > $moment->dateTime);
     }
 
+    public function isNotAfterInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isNotAfter($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isNotAfter($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isNotAfter($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isNotAfter($comparator),
+        };
+    }
+
     public function isAfterOrEqualTo(self $moment): bool
     {
         return $this->dateTime >= $moment->dateTime;
     }
 
+    public function isAfterOrEqualToInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isAfterOrEqualTo($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isAfterOrEqualTo($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isAfterOrEqualTo($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isAfterOrEqualTo($comparator),
+        };
+    }
+
     public function isNotAfterOrEqualTo(self $moment): bool
     {
         return !($this->dateTime >= $moment->dateTime);
     }
 
+    public function isNotAfterOrEqualToInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isNotAfterOrEqualTo($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isNotAfterOrEqualTo($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isNotAfterOrEqualTo($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isNotAfterOrEqualTo($comparator),
+        };
+    }
+
     public function isBeforeOrEqualTo(self $moment): bool
     {
         return $this->dateTime <= $moment->dateTime;
     }
 
+    public function isBeforeOrEqualToInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isBeforeOrEqualTo($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isBeforeOrEqualTo($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isBeforeOrEqualTo($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isBeforeOrEqualTo($comparator),
+        };
+    }
+
     public function isNotBeforeOrEqualTo(self $moment): bool
     {
         return !($this->dateTime <= $moment->dateTime);
     }
 
-    public function isBefore(self $moment): bool
-    {
-        return $this->dateTime < $moment->dateTime;
+    public function isNotBeforeOrEqualToInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isNotBeforeOrEqualTo($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isNotBeforeOrEqualTo($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isNotBeforeOrEqualTo($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isNotBeforeOrEqualTo($comparator),
+        };
+    }
+
+    public function isBefore(
+        self $before,
+    ): bool {
+        return $this->dateTime < $before->dateTime;
+    }
+
+    public function isBeforeInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isBefore($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isBefore($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isBefore($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isBefore($comparator),
+        };
     }
 
     public function isNotBefore(self $moment): bool
@@ -142,11 +251,26 @@ public function isNotBefore(self $moment): bool
         return !($this->dateTime < $moment->dateTime);
     }
 
+    public function isNotBeforeInTimeZone(
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): bool {
+        return match (true) {
+            $comparator instanceof Time => $this->timeInTimeZone($timeZone)->isNotBefore($comparator),
+            $comparator instanceof Date => $this->dateInTimeZone($timeZone)->isNotBefore($comparator),
+            $comparator instanceof Month => $this->monthInTimeZone($timeZone)->isNotBefore($comparator),
+            $comparator instanceof Year => $this->yearInTimeZone($timeZone)->isNotBefore($comparator),
+        };
+    }
+
     public function compareTo(self $moment): int
     {
         return $this->dateTime <=> $moment->dateTime;
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isAfterInTimeZone instead.
+     */
     public function isDateAfterInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isAfter(
@@ -154,6 +278,9 @@ public function isDateAfterInTimeZone(self $moment, \DateTimeZone $timeZone): bo
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isNotAfterInTimeZone instead.
+     */
     public function isDateNotAfterInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isNotAfter(
@@ -161,6 +288,9 @@ public function isDateNotAfterInTimeZone(self $moment, \DateTimeZone $timeZone):
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isAfterOrEqualToInTimeZone instead.
+     */
     public function isDateAfterOrEqualToInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isAfterOrEqualTo(
@@ -168,6 +298,9 @@ public function isDateAfterOrEqualToInTimeZone(self $moment, \DateTimeZone $time
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isNotAfterOrEqualToInTimeZone instead.
+     */
     public function isDateNotAfterOrEqualToInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isNotAfterOrEqualTo(
@@ -175,6 +308,9 @@ public function isDateNotAfterOrEqualToInTimeZone(self $moment, \DateTimeZone $t
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isEqualToInTimeZone instead.
+     */
     public function isDateEqualToInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isEqualTo(
@@ -182,6 +318,9 @@ public function isDateEqualToInTimeZone(self $moment, \DateTimeZone $timeZone):
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isNotEqualToInTimeZone instead.
+     */
     public function isDateNotEqualToInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isNotEqualTo(
@@ -189,6 +328,9 @@ public function isDateNotEqualToInTimeZone(self $moment, \DateTimeZone $timeZone
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isBeforeInTimeZone instead.
+     */
     public function isDateBeforeInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isBefore(
@@ -196,6 +338,9 @@ public function isDateBeforeInTimeZone(self $moment, \DateTimeZone $timeZone): b
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isNotBeforeInTimeZone instead.
+     */
     public function isDateNotBeforeInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isNotBefore(
@@ -203,6 +348,9 @@ public function isDateNotBeforeInTimeZone(self $moment, \DateTimeZone $timeZone)
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isBeforeOrEqualToInTimeZone instead.
+     */
     public function isDateBeforeOrEqualToInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isBeforeOrEqualTo(
@@ -210,6 +358,9 @@ public function isDateBeforeOrEqualToInTimeZone(self $moment, \DateTimeZone $tim
         );
     }
 
+    /**
+     * @deprecated Will be removed in one of the next minor releases. Use @see Date->isNotBeforeOrEqualToInTimeZone instead.
+     */
     public function isDateNotBeforeOrEqualToInTimeZone(self $moment, \DateTimeZone $timeZone): bool
     {
         return $this->dateInTimeZone($timeZone)->isNotBeforeOrEqualTo(
diff --git a/src/Time.php b/src/Time.php
index 87ea082..7d097c8 100644
--- a/src/Time.php
+++ b/src/Time.php
@@ -86,72 +86,52 @@ public function __toString(): string
 
     public function isAfter(self $time): bool
     {
-        $thisAsDateTime = $this->toDateTimeImmutable();
-        $comparedTo = $thisAsDateTime->setTime(
-            $time->hour,
-            $time->minute,
-            $time->second,
-            $time->microsecond,
-        );
+        return $this->toDateTimeImmutable() > $time->toDateTimeImmutable();
+    }
 
-        return $thisAsDateTime > $comparedTo;
+    public function isNotAfter(self $time): bool
+    {
+        return !($this->toDateTimeImmutable() > $time->toDateTimeImmutable());
     }
 
     public function isAfterOrEqualTo(self $time): bool
     {
-        $thisAsDateTime = $this->toDateTimeImmutable();
-        $comparedTo = $thisAsDateTime->setTime(
-            $time->hour,
-            $time->minute,
-            $time->second,
-            $time->microsecond,
-        );
+        return $this->toDateTimeImmutable() >= $time->toDateTimeImmutable();
+    }
 
-        return $thisAsDateTime >= $comparedTo;
+    public function isNotAfterOrEqualTo(self $time): bool
+    {
+        return !($this->toDateTimeImmutable() >= $time->toDateTimeImmutable());
     }
 
     public function isEqualTo(self $time): bool
     {
-        $thisAsDateTime = $this->toDateTimeImmutable();
-        $comparedTo = $thisAsDateTime->setTime(
-            $time->hour,
-            $time->minute,
-            $time->second,
-            $time->microsecond,
-        );
-
-        return $thisAsDateTime == $comparedTo;
+        return $this->toDateTimeImmutable() == $time->toDateTimeImmutable();
     }
 
     public function isNotEqualTo(self $time): bool
     {
-        return !$this->isEqualTo($time);
+        return $this->toDateTimeImmutable() != $time->toDateTimeImmutable();
     }
 
     public function isBefore(self $time): bool
     {
-        $thisAsDateTime = $this->toDateTimeImmutable();
-        $comparedTo = $thisAsDateTime->setTime(
-            $time->hour,
-            $time->minute,
-            $time->second,
-            $time->microsecond,
-        );
+        return $this->toDateTimeImmutable() < $time->toDateTimeImmutable();
+    }
 
-        return $thisAsDateTime < $comparedTo;
+    public function isNotBefore(self $time): bool
+    {
+        return !($this->toDateTimeImmutable() < $time->toDateTimeImmutable());
     }
 
     public function isBeforeOrEqualTo(self $time): bool
     {
-        $thisAsDateTime = $this->toDateTimeImmutable();
-        $comparedTo = $thisAsDateTime->setTime(
-            $time->hour,
-            $time->minute,
-            $time->second,
-            $time->microsecond,
-        );
+        return $this->toDateTimeImmutable() <= $time->toDateTimeImmutable();
+    }
 
-        return $thisAsDateTime <= $comparedTo;
+    public function isNotBeforeOrEqualTo(self $time): bool
+    {
+        return !($this->toDateTimeImmutable() <= $time->toDateTimeImmutable());
     }
 
     public function isMidnight(): bool
@@ -164,7 +144,10 @@ public function isMidnight(): bool
 
     public function isNotMidnight(): bool
     {
-        return !$this->isMidnight();
+        return $this->hour !== 0
+            || $this->minute !== 0
+            || $this->second !== 0
+            || $this->microsecond !== 0;
     }
 
     public function compareTo(self $time): int
diff --git a/tests/Moment/IsAfterInTimeZoneTest.php b/tests/Moment/IsAfterInTimeZoneTest.php
new file mode 100644
index 0000000..bafc998
--- /dev/null
+++ b/tests/Moment/IsAfterInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsAfterInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isAfterInTimeZone
+     */
+    public function is_after_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isAfterInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                false,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                true,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsAfterOrEqualToInTimeZoneTest.php b/tests/Moment/IsAfterOrEqualToInTimeZoneTest.php
new file mode 100644
index 0000000..45d76d6
--- /dev/null
+++ b/tests/Moment/IsAfterOrEqualToInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsAfterOrEqualToInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isAfterOrEqualToInTimeZone
+     */
+    public function is_after_or_equal_to_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isAfterOrEqualToInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                false,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                true,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsBeforeInTimeZoneTest.php b/tests/Moment/IsBeforeInTimeZoneTest.php
new file mode 100644
index 0000000..197c34d
--- /dev/null
+++ b/tests/Moment/IsBeforeInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsBeforeInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isBeforeInTimeZone
+     */
+    public function is_before_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isBeforeInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                true,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                false,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsBeforeOrEqualToInTimeZoneTest.php b/tests/Moment/IsBeforeOrEqualToInTimeZoneTest.php
new file mode 100644
index 0000000..17066f7
--- /dev/null
+++ b/tests/Moment/IsBeforeOrEqualToInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsBeforeOrEqualToInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isBeforeOrEqualToInTimeZone
+     */
+    public function is_before_or_equal_to_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isBeforeOrEqualToInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                true,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                false,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsEqualToInTimeZoneTest.php b/tests/Moment/IsEqualToInTimeZoneTest.php
new file mode 100644
index 0000000..af1ea6f
--- /dev/null
+++ b/tests/Moment/IsEqualToInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsEqualToInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isEqualToInTimeZone
+     */
+    public function is_equal_to_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isEqualToInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                false,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                false,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsNotAfterInTimeZoneTest.php b/tests/Moment/IsNotAfterInTimeZoneTest.php
new file mode 100644
index 0000000..829ab0c
--- /dev/null
+++ b/tests/Moment/IsNotAfterInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsNotAfterInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotAfterInTimeZone
+     */
+    public function is_not_after_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isNotAfterInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                true,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                false,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsNotAfterOrEqualToInTimeZoneTest.php b/tests/Moment/IsNotAfterOrEqualToInTimeZoneTest.php
new file mode 100644
index 0000000..21f0a7b
--- /dev/null
+++ b/tests/Moment/IsNotAfterOrEqualToInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsNotAfterOrEqualToInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotAfterOrEqualToInTimeZone
+     */
+    public function is_not_after_or_equal_to_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isNotAfterOrEqualToInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                true,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                false,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsNotBeforeInTimeZoneTest.php b/tests/Moment/IsNotBeforeInTimeZoneTest.php
new file mode 100644
index 0000000..0a1bcd1
--- /dev/null
+++ b/tests/Moment/IsNotBeforeInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsNotBeforeInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotBeforeInTimeZone
+     */
+    public function is_not_before_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isNotBeforeInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                false,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                true,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsNotBeforeOrEqualToInTimeZoneTest.php b/tests/Moment/IsNotBeforeOrEqualToInTimeZoneTest.php
new file mode 100644
index 0000000..7d28831
--- /dev/null
+++ b/tests/Moment/IsNotBeforeOrEqualToInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsNotBeforeOrEqualToInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotBeforeOrEqualToInTimeZone
+     */
+    public function is_not_before_or_equal_to_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isNotBeforeOrEqualToInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                false,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                true,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Moment/IsNotEqualToInTimeZoneTest.php b/tests/Moment/IsNotEqualToInTimeZoneTest.php
new file mode 100644
index 0000000..9dac15b
--- /dev/null
+++ b/tests/Moment/IsNotEqualToInTimeZoneTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Moment;
+
+use DigitalCraftsman\DateTimePrecision\Date;
+use DigitalCraftsman\DateTimePrecision\Moment;
+use DigitalCraftsman\DateTimePrecision\Month;
+use DigitalCraftsman\DateTimePrecision\Time;
+use DigitalCraftsman\DateTimePrecision\Year;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Moment */
+final class IsNotEqualToInTimeZoneTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotEqualToInTimeZone
+     */
+    public function is_not_equal_to_in_time_zone_works(
+        bool $expectedResult,
+        Moment $moment,
+        Time | Date | Month | Year $comparator,
+        \DateTimeZone $timeZone,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $moment->isNotEqualToInTimeZone($comparator, $timeZone));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Moment,
+     *   2: Time | Date | Month | Year,
+     *   3: \DateTimeZone,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            'moment before time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 14:55:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same time' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after time' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-08 15:05:00', new \DateTimeZone('Europe/Berlin')),
+                Time::fromString('15:00:00'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-07 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same date' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after date' => [
+                true,
+                Moment::fromStringInTimeZone('2022-10-09 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Date::fromString('2022-10-08'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-09-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same month' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after month' => [
+                true,
+                Moment::fromStringInTimeZone('2022-11-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Month::fromString('2022-10'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment before year' => [
+                true,
+                Moment::fromStringInTimeZone('2021-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment same year' => [
+                false,
+                Moment::fromStringInTimeZone('2022-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+            'moment after year' => [
+                true,
+                Moment::fromStringInTimeZone('2023-10-08 15:00:00', new \DateTimeZone('Europe/Berlin')),
+                Year::fromString('2022'),
+                new \DateTimeZone('Europe/Berlin'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Time/IsNotAfterOrEqualToTest.php b/tests/Time/IsNotAfterOrEqualToTest.php
new file mode 100644
index 0000000..7256fdb
--- /dev/null
+++ b/tests/Time/IsNotAfterOrEqualToTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Time;
+
+use DigitalCraftsman\DateTimePrecision\Time;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Time */
+final class IsNotAfterOrEqualToTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotAfterOrEqualTo
+     */
+    public function is_not_after_or_equal_to_works(
+        bool $expectedResult,
+        Time $time,
+        Time $comparator,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $time->isNotAfterOrEqualTo($comparator));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Time,
+     *   2: Time,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            '1 hour before' => [
+                true,
+                Time::fromString('15:00:00'),
+                Time::fromString('16:00:00'),
+            ],
+            'same time' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 hour later' => [
+                false,
+                Time::fromString('16:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 minute later' => [
+                false,
+                Time::fromString('15:01:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 second later' => [
+                false,
+                Time::fromString('15:00:01'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 millisecond later' => [
+                false,
+                Time::fromString('15:00:00.000001'),
+                Time::fromString('15:00:00'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Time/IsNotAfterTest.php b/tests/Time/IsNotAfterTest.php
new file mode 100644
index 0000000..022f2d8
--- /dev/null
+++ b/tests/Time/IsNotAfterTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Time;
+
+use DigitalCraftsman\DateTimePrecision\Time;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Time */
+final class IsNotAfterTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotAfter
+     */
+    public function is_not_after_works(
+        bool $expectedResult,
+        Time $time,
+        Time $comparator,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $time->isNotAfter($comparator));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Time,
+     *   2: Time,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            '1 hour before' => [
+                true,
+                Time::fromString('15:00:00'),
+                Time::fromString('16:00:00'),
+            ],
+            'same time' => [
+                true,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 hour later' => [
+                false,
+                Time::fromString('16:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 minute later' => [
+                false,
+                Time::fromString('15:01:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 second later' => [
+                false,
+                Time::fromString('15:00:01'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 millisecond later' => [
+                false,
+                Time::fromString('15:00:00.000001'),
+                Time::fromString('15:00:00'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Time/IsNotBeforeOrEqualToTest.php b/tests/Time/IsNotBeforeOrEqualToTest.php
new file mode 100644
index 0000000..da1df08
--- /dev/null
+++ b/tests/Time/IsNotBeforeOrEqualToTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Time;
+
+use DigitalCraftsman\DateTimePrecision\Time;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Time */
+final class IsNotBeforeOrEqualToTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotBeforeOrEqualTo
+     */
+    public function is_not_before_or_equal_to_works(
+        bool $expectedResult,
+        Time $time,
+        Time $comparator,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $time->isNotBeforeOrEqualTo($comparator));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Time,
+     *   2: Time,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            '1 hour after' => [
+                true,
+                Time::fromString('16:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            'same time' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 hour before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('16:00:00'),
+            ],
+            '1 minute before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:01:00'),
+            ],
+            '1 second before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:01'),
+            ],
+            '1 millisecond before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:00.000001'),
+            ],
+        ];
+    }
+}
diff --git a/tests/Time/IsNotBeforeTest.php b/tests/Time/IsNotBeforeTest.php
new file mode 100644
index 0000000..4df138f
--- /dev/null
+++ b/tests/Time/IsNotBeforeTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DigitalCraftsman\DateTimePrecision\Time;
+
+use DigitalCraftsman\DateTimePrecision\Time;
+use PHPUnit\Framework\TestCase;
+
+/** @coversDefaultClass \DigitalCraftsman\DateTimePrecision\Time */
+final class IsNotBeforeTest extends TestCase
+{
+    /**
+     * @test
+     *
+     * @dataProvider dataProvider
+     *
+     * @covers ::isNotBefore
+     */
+    public function is_not_before_works(
+        bool $expectedResult,
+        Time $time,
+        Time $comparator,
+    ): void {
+        // -- Act & Assert
+        self::assertSame($expectedResult, $time->isNotBefore($comparator));
+    }
+
+    /**
+     * @return array<string, array{
+     *   0: boolean,
+     *   1: Time,
+     *   2: Time,
+     * }>
+     */
+    public static function dataProvider(): array
+    {
+        return [
+            '1 hour after' => [
+                true,
+                Time::fromString('16:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            'same time' => [
+                true,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:00'),
+            ],
+            '1 hour before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('16:00:00'),
+            ],
+            '1 minute before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:01:00'),
+            ],
+            '1 second before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:01'),
+            ],
+            '1 millisecond before' => [
+                false,
+                Time::fromString('15:00:00'),
+                Time::fromString('15:00:00.000001'),
+            ],
+        ];
+    }
+}