There is a task to describe the interval by means of OOP. In fact, the question is not quite how, but the question is - are there any ready-made solutions? Programming has existed for decades and this task should have arisen repeatedly and, accordingly, there should be a general solution of which I apparently don’t know.

It is clear that the interval is determined by 2 points and a set of points on the number line between them. If we have an interval, then it is logical that we can check whether a certain point is in a given interval. And then there are problems, since the intervals are of 4 types - [a, b] , (a, b) , [a, b) , (a, b] .

Also the point does not have to be a number. It can be a date, time or even IP. From here there is a heap of combinations and complexity in configuring. Maybe there is something ready?

Updated

In my case, the PHP language, but a solution in Java or C # will do. I would have a general principle.

  • Want to ready - specify the programming language - Dmitriy Simushev
  • @DmitriySimushev in my case, PHP, but the solution is not necessarily to give on it - ghost404

3 answers 3

Let's start with dealing with your problem from the end.

Also the point does not have to be a number. It can be a date, time or even IP. From here there is a heap of combinations and complexity in configuring. Maybe there is something ready?

In all programming languages, there are comparison operations. It always works for numbers, but there may be problems with other data types.

If you are lucky and your programming language supports the redefinition of comparison operations, then you can enter your data type (for example, IPAddress ) and implement for it the correct behavior of the > , < , == operators (all other comparison operators are derivatives of these three).

If you are unlucky and your programming language does not support the redefinition of comparison operators, then a special interface will come to your aid, containing the necessary methods in an explicit form:

 interface IPPointInterface { public function isEqualTo(IPPointInterface $ip): bool; public function isGraterThan(IPPointInterface $ip): bool; public function isLessThan(IPPointInterface $ip): bool; } 

By analogy, you can enter an interface for any data type.

It is clear that the interval is determined by 2 points and a set of points on the number line between them. If we have an interval, then it is logical that we can check whether a certain point is in a given interval.

You have correctly captured the main essence of the interval - it can check whether a point is included in it or not. That is what you should reflect in its interface. In addition, you need to provide an opportunity for client code to get start and end points:

 interface IPIntervalInterface { public function containsPoint(IPPointInterface $ip): bool; public function getStartPoint(IPPointInterface $ip): IPPoint; public function getEndPoint(IPPointInterface $ip): IPPoint; } 

From personal experience, I want to add that in 99% of cases there are a few more methods that the interface of the interval should support, for example, checking the intersection with another interval.

And then there are problems, since the intervals are of 4 types - [a, b] , (a, b) , [a, b) , (a, b] .

And this is the implementation details. You can either implement the four classes of intervals or cheat and implement the base class for the segment (closed interval [a, b] ):

 class IPClosedInterval implements IPIntervalInterface { private $start; private $end; public function __construct(IPPointInterface $start, IPPointInterface $end) { $this->start = $start; $this->end = $end; } public function getStartPoint() { return $this->start; } public function getEndPoint() { return $this->end; } public function containsPoint(IPPointInterface $ip) { return $ip->isLessThan($this->getEndPoint()) && $ip->isGreaterThan($this->getStartEndPoint()); } } 

And two decorators "opening" the border (I will give the code only for the "opening" of the left border):

 class IPIntervalLeftOpenDecorator implements IPIntervalInterface { private $interval; public function __construct(IPIntervalInterface $interval) { $this->interval = $interval; } public function containsPoint(IPPointInterface $ip) { return $this->interval->containsPoint($ip) || $ip->isEqualTo($this->getStartPoint()); } public function getStartPoint() { return $this->interval->getStartPoint(); } public function getEndPoint() { return $this->interval->getEndPoint(); } } 

If desired, the constructor and the code that returns the borders can be rendered into an abstract decorator.

Here is an example of how this can be used:

 $a = new IPPoint('...'); $b = new IPPoint('...'); $closed_int = new IPIntervalClosed($a, $b); // [a, b] $left_opened_int = new IPIntervalLeftOpenDecorator($closed_int); // (a, b] $right_opened_int = new IPIntervalRightOpenDecorator($closed_int); // [a, b) $opened_int = new IPIntervalLeftOpenDecorator($right_opened_int); // (a, b) 

Programming has existed for decades and this task should have arisen repeatedly and, accordingly, there should be a general solution of which I apparently don’t know.

It very much depends on the programming language you use. For example, in C ++ you can define a base type for a point and parameterize the interval class with this type.

As for PHP, here you can also define the base type for the point (completely similar to my IPPointInterface except for the types of the method arguments), but then you have to manually control the match of the compared types in the interval methods. This is not as difficult as it seems, but I still prefer automatic type checking.

If you still need automatic type checking in PHP, you can implement automatic generation of interval classes for different types of points, but this topic needs a separate discussion.

In fact, the question is not quite how, but the question is - are there any ready-made solutions?

Personally, I only came across a class for working with date intervals: league / period . At the same time, I never looked for any solution to work with other types of intervals.

  • it does not fit here, because it added an example of its implementation to the main question - ghost404
  • Decorators are not very suitable as decorators need to be created for each type of interval, which is already redundant and complicates the configuration of intervals. - ghost404 2:29
  • @ ghost404, to be honest, I did not understand why you disfigured the question by adding an answer to it. As for the decorators - this is only one of the solutions. I also mentioned the autogeneration of the code of intervals for each of the points, perhaps this way will help you - Dmitriy Simushev
  • @ ghost404 so far as ValueObject is concerned, yes, intervals and points are good candidates for this. Only you need to remember that, for example, in Doctrine - you will not be able to implement inheritance for Embeddable classes ... - Dmitriy Simushev
  • This is what I don't like about stackoverflow, so you can't write a detailed comment. As a result, you have to either add to the question or write your answer which is not the answer to the question - ghost404

Learning by learning, but it must be remembered that in a more applied reality a lot of OOP for the sake of the PLO easily develops into an antipattern.

Also, much depends on the application. For example, if you need a fairly wide range of operations on, for example, intersection or union, if possible, of intervals, then different classes for different types of openness of intervals, or bells and whistles on the base class of decorators can only grind out a result without a useful exhaust.

I would do something like this:

  abstract class AbstractValue { protected $value; public function __construct($value) { $this->value = $value; } public function __toString() { return strval($this->value); } public function getValue() { return $this->value; } public function setValue($value) { $this->value = $value; return $this; } protected function compareCheck(AbstractValue $b) { $class = get_class($this); if (!$b instanceof $class) { throw new \Exception("Uncomparable entities: " . get_class($b) . " vs " . get_class($this)); } } final public function compareWith(AbstractValue $b) { $this->compareCheck($b); return $this->doCompareWith($b); } protected function doCompareWith(AbstractValue $b) { $diff = $this->value - $b->getValue(); return (int)$diff; } final public function isEqualTo(AbstractValue $b) { $this->compareCheck($b); return $this->doIsEqualTo($b); } protected function doIsEqualTo(AbstractValue $b) { return $this->value == $b->getValue(); } final public function isGreaterThan(AbstractValue $b) { $this->compareCheck($b); return $this->doIsGreaterThan($b); } protected function doIsGreaterThan(AbstractValue $b) { return $this->value > $b->getValue(); } final public function isLessThan(AbstractValue $b) { $this->compareCheck($b); return $this->doIsLessThan($b); } protected function doIsLessThan(AbstractValue $b) { return $this->value < $b->getValue(); } } class Interval { const TYPE_CLOSED = 0; const TYPE_START_EXCLUDED = 1; const TYPE_END_EXCLUDED = 2; const TYPE_OPEN = 3; // = TYPE_START_EXCLUDED | TYPE_END_EXCLUDED private $start; private $end; private $type; private $isStartExcluded = false; private $isEndExcluded = false; public function __construct(AbstractValue $start, AbstractValue $end, $type = self::TYPE_CLOSED) { // We already sure that "start" & "end" is like same type and comparable if ($start->isLessThan($end)) { $this->start = $start; $this->end = $end; } else { $this->start = $end; $this->end = $start; } $this->type = $type; // We can add a filter for bad values here $this->isStartExcluded = (bool) ($type & self::TYPE_START_EXCLUDED); $this->isEndExcluded = (bool) ($type & self::TYPE_END_EXCLUDED); } public function __toString() { return ($this->isStartExcluded ? '(' : '[') . $this->start . ', ' . $this->end . ($this->isEndExcluded ? ')' : ']'); } public function getStart() { return $this->start; } public function setStart($start) { if ($start->isLessThan($this->end)) { $this->start = $start; } else { $this->start = $this->end; $this->end = $start; } return $this; } public function getEnd() { return $this->end; } public function setEnd($end) { if ($this->start->isLessThan($end)) { $this->end = $end; } else { $this->end = $this->start; $this->start = $end; } return $this; } public function getType() { return $this->type; } public function setType($type) { $this->type = $type; // We can add a filter for bad values here $this->isStartExcluded = (bool) ($type & self::TYPE_START_EXCLUDED); $this->isEndExcluded = (bool) ($type & self::TYPE_END_EXCLUDED); return $this; } public function getIsStartExcluded() { return $this->isStartExcluded; } public function setIsStartExcluded($value) { $value = (bool) $value; $this->isStartExcluded = $value; $this->type = $value ? ($type | TYPE_START_EXCLUDED) : ($type & ~TYPE_START_EXCLUDED); return $this; } public function getIsEndExcluded() { return $this->isEndExcluded; } public function setIsEndExcluded($value) { $value = (bool) $value; $this->isEndExcluded = $value; $this->type = $value ? ($type | TYPE_END_EXCLUDED) : ($type & ~TYPE_END_EXCLUDED); return $this; } public function contains(AbstractValue $point) { $cmpStart = $point->compareWith($this->start); if ($cmpStart < 0) return false; $cmpEnd = $point->compareWith($this->end); if ($cmpEnd > 0) return false; if ($this->isStartExcluded && $cmpStart == 0) return false; if ($this->isEndExcluded && $cmpEnd == 0) return false; return true; } public function intersect(Interval $second) { $cmp_start2_start1 = $second->getStart()->compareWith($this->start); $cmp_end2_end1 = $second->getEnd()->compareWith($this->end); if ($cmp_start2_start1 > 0) { $start = $second->getStart(); $isStartExcluded = $second->getIsStartExcluded(); } else { $start = $this->start; $isStartExcluded = $cmp_start2_start1 < 0 ? $this->isStartExcluded : $this->isStartExcluded || $second->getIsStartExcluded(); } if ($cmp_end2_end1 < 0) { $end = $second->getEnd(); $isEndExcluded = $second->getIsEndExcluded(); } else { $end = $this->end; $isEndExcluded = $cmp_end2_end1 > 0 ? $this->isEndExcluded : $this->isEndExcluded || $second->getIsEndExcluded(); } $type = $isStartExcluded ? Interval::TYPE_START_EXCLUDED : 0; $type |= ($isEndExcluded ? Interval::TYPE_END_EXCLUDED : 0); $cmp_start_end = $start->compareWith($end); // single point real interval if ($cmp_start_end == 0 && $type == Interval::TYPE_CLOSED) { return new Interval($start, $start, Interval::TYPE_CLOSED); } // null-interval (any) of current type if ($cmp_start_end >= 0) { return new Interval($start, $start, Interval::TYPE_OPEN); } return new Interval($start, $end, $type); } } class MathNumber extends AbstractValue { } class DateTimePoint extends AbstractValue { public function __construct(\DateTime $value) { parent::__construct($value); } public function __toString() { return $this->value->format('Ymd H:i:s'); } public function setValue(\DateTime $value) { return parent::setValue($value); } public function doCompareWith(DateTimePoint $b) { if ($this->getValue() > $b->getValue()) { return 1; } if ($this->getValue() < $b->getValue()) { return -1; } return 0; } } 

Everything quite nicely works like this:

Numbers:

  echo "<pre>"; $n1 = new MathNumber(10); $n2 = new MathNumber(2.5); $a = new Interval($n1, $n2); $a = new Interval($n1, $n2, Interval::TYPE_START_EXCLUDED); $n3 = new MathNumber(20); $a->setEnd($n3)->setIsEndExcluded(true); echo "Value " . $n2 . " is " . ($a->contains($n2) ? "" : "NOT ") . "in " . $a . "\n"; echo "Value " . $n1 . " is " . ($a->contains($n1) ? "" : "NOT ") . "in " . $a . "\n"; echo "\n"; 

Value 2.5 is NOT in (2.5, 20)

Value 10 is in (2.5, 20)

And it’s also fun to cut the segments:

  $n4 = new MathNumber(12); $n5 = new MathNumber(55); $a2 = new Interval($n4, $n5); $a3 = $a->intersect($a2); echo "Value " . $n3 . " is " . ($a2->contains($n3) ? "" : "NOT ") . "in " . $a2 . "\n"; echo "Value " . $n3 . " is " . ($a3->contains($n3) ? "" : "NOT ") . "in " . $a3 . " - intersect \n"; echo "\n"; 

Value 20 is in [12, 55]

Value 20 is NOT in [12, 20) - intersect

Dates:

  $dt1 = new DateTimePoint(new \DateTime()); $dt2 = new DateTimePoint(new \DateTime('+30 days')); $dt3 = new DateTimePoint(new \DateTime('+5 days')); $b = new Interval($dt1, $dt2, Interval::TYPE_OPEN); echo "Value " . $dt1 . " is " . ($b->contains($dt1) ? "" : "NOT ") . "in " . $b . "\n"; echo "Value " . $dt3 . " is " . ($b->contains($dt3) ? "" : "NOT ") . "in " . $b . "\n"; echo "\n"; 

Value 2016-11-11 03:26:13 is NOT in (2016-11-11 03:26:13, 2016-12-11 03:26:13)

Value 2016-11-16 03:26:13 is in (2016-11-11 03:26:13, 2016-12-11 03:26:13)

We try to blind the interval from the number and date - we get as it was intended by scham ekscepshnom:

  $c = new Interval($n1, $dt2); echo "Value " . $dt1 . " is " . ($c->contains($dt1) ? "" : "NOT ") . "in " . $c . "\n"; 

Uncomparable entities: DateTimePoint vs MathNumber

It is possible to be confused with abstract Intervals, interfaces, treit, of which only. But why?

Well, if you do not need and will not have any common approach in an applied task, and, for example, everything is exactly as in the initial formulation of the problem: mathematical segments, only double and nothing more, and it will be necessary to calculate a lot and quickly .. Write the code . Quick code. Do not be lazy. Half the PLO trash for such a specific purpose is not something that is not needed, but harmful. But OOP is generally a useful thing in many cases.

Successes!

  • Yes, of course, for intervals you will definitely need, for example: a method for obtaining the length of this interval. For [10, 51) it will be 41, for dates it must be assumed must return - FlameStorm

The type of the interval I rendered to ValueObject and the verification operation was done through it. The result is such an interface:

 interface IntervalTypeInterface { public function intervalContains(IntervalInterface $interval, $point); } 

The interval itself has an interface:

 interface IntervalInterface { public function contains($point); public function start(); public function end(); } 

An interesting idea is to do it through the decalators, but this is not appropriate since the interval itself must know its type (closed / open) and the code that will work at this interval is expected by a specific type of interval. Ie, the user code will wait for DateTimeInterval and an attempt to pass it IPInterval should result in an error. And if we are going to create specific IPClosedInterval type IPClosedInterval , then such kind of IPClosedInterval need to be created for each type of interval, which is already redundant and complicates the configuration of intervals.

Here is an example of the implementation of the date interval:

 class DateTimeInterval implements IntervalInterface { private $type; private $start; private $end; public function __construct(IntervalTypeInterface $type, \DateTime $start, \DateTime $end) { // это должен быть корректный интервал if ($start >= $end) { throw IncorrectIntervalException::create(); } $this->type = $type; $this->start = clone $start; $this->end = clone $end; } public function contains($point) { // здесь я вынужден проверять тип данных точки // ожидается \DateTime, но может прийти все что угодно $this->checkPointType($point); return $this->type->intervalContains($this, $point); } // ... } 

That was actually the problem. I have to explicitly check the data type of the point. PHP does not allow you to override the parameter type in the inherited class. Ie I have to stop using IntervalInterface and, accordingly, there are problems in using the IntervalTypeInterface interface.

There was another idea. Just wrap the value of the interval point in the class and all the comparison operations to shift to it. Points are better known for comparing them. The same IP is a string and for comparison in PHP there is a function that will help with this.

But we have a problem associated with the fact that each point must be wrapped in order to use in the interval. For IP this is the only possible option, but for numbers and dates (PHP allows you to perform comparison operations for date objects) is not necessary. Then you can only wrap an IP point into an object, but then we get a discrepancy in style.

And this does not solve the problem with the type of interval. There are only 4 interval types. No more and no less. They perform the same comparison actions for all types of points. In this case, it is logical to make a common interface for points:

 interface PointInterface { public function isEqualTo(PointInterface $point): bool; public function isGreaterThan(PointInterface $point): bool; public function isLessThan(PointInterface $point): bool; } 

The type will have an interface:

 interface IntervalTypeInterface { public function intervalContains(IntervalInterface $interval, PointInterface $point); } 

Well, an example of the implementation of an open interval

 class OpenInterval implements IntervalTypeInterface { public function intervalContains(IntervalInterface $interval, PointInterface $point) { return $interval->start()->isLessThan($point) && $interval->end()->isGreaterThan($point); } } 

It seems to be better, but PHP does not allow overriding the type of the parameters of the methods, so you can not do this:

 interface IPPointInterface extends PointInterface { public function isEqualTo(IPPointInterface $ip): bool; public function isGreaterThan(IPPointInterface $ip): bool; public function isLessThan(IPPointInterface $ip): bool; } 

And it turns out inside the point we will have to explicitly check the data type, which is not very good. And it turns out the only option that I see so far is the use of switch / case in the interval:

 class IPInterval { private $type; private $start; private $end; const TYPE_CLOSED = 'closed'; const TYPE_OPEN = 'open'; const TYPE_HALF_CLOSED = 'half-closed'; const TYPE_HALF_OPEN = 'half-open'; public function __construct(IPPointInterface $start, IPPointInterface $end, $type = self::TYPE_CLOSED) { // проверка допустимости типа интервала if (!in_array($type, self::types())) { throw IncorrectIntervalTypeException::create(); } // это должен быть корректный интервал if ($start->isEqualTo($end) || $start->isGreaterThan($end)) { throw IncorrectIntervalException::create(); } $this->type = $type; $this->start = $start; $this->end = $end; } public start function types() // можно вынести в базовый класс { return [ self::TYPE_CLOSED, self::TYPE_OPEN, self::TYPE_HALF_CLOSED, self::TYPE_HALF_OPEN, ]; } public function contains(IPPointInterface $point) { // этот код дублируется для всех типов интервалов если мы будем оборачивать точки в классы // если будем оборачивать только IP то дублироваться будет в интервалах дат, времени и числовых интервалах switch ($this->type) { case self::TYPE_OPEN: return $this->start->isLessThan($point) && $this->end->isGreaterThan($point); // дальше по аналогии } } } 

It seems to be a working version, but some kind of clumsy one.

You can make a generic ValueObject interval type for each interval type:

 class IPIntervalType { const TYPE_CLOSED = 'closed'; const TYPE_OPEN = 'open'; const TYPE_HALF_CLOSED = 'half-closed'; const TYPE_HALF_OPEN = 'half-open'; private function __construct($type) { // тип можно не проверять так как тип определяется только через фабричные методы $this->type = $type; } // фабричный метод public static function closed() { return new self(self::TYPE_CLOSED); } // другие фабричные методы // .. // все что выше можно вынести в базовый класс public function intervalContains(IPIntervalInterface $interval, IPPointInterface $point) { return $interval->start()->isLessThan($point) && $interval->end()->isGreaterThan($point); } } 

then in the interval we get the simplification

 class IPInterval { // .. public function contains(IPPointInterface $point) { return $this->type->intervalContains($this, $point); } }