Skip to content

Commit 5acf299

Browse files
committed
Merge branch 'feature/56-add-integer-range-adv-record' into develop
Fixes #56
2 parents 5eb4162 + 36f7128 commit 5acf299

9 files changed

+1629
-8
lines changed

collection/706.dat

+261
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
type
2+
TIntegerRange = record
3+
strict private
4+
var
5+
fLowerBound: Integer;
6+
fUpperBound: Integer;
7+
function GetLowerBound: Integer;
8+
function GetUpperBound: Integer;
9+
function IsSubrangeOf(const ARange: TIntegerRange): Boolean;
10+
public
11+
// Constructs a range whose bounds are A and B. The lowest of the two
12+
// parameters is taken as the lower bound of the range with the other
13+
// parameter taken as the upper bound.
14+
// Valid bounds must fall in the range -MaxInt..MaxInt. An
15+
// EArgumentException exception is raised otherwise.
16+
constructor Create(const A, B: Integer);
17+
18+
// Constructs an empty range.
19+
class function CreateEmpty: TIntegerRange; static;
20+
21+
// Checks if the range is empty.
22+
function IsEmpty: Boolean;
23+
24+
// Returns the length of the range, i.e. the number of integers in the range.
25+
function Length: Cardinal;
26+
27+
// Constrains AValue to fall within this range. If the value lies within the
28+
// range it is returned unchanged. If it is outside the range then either
29+
// LowerBound or UpperBound is returned, depending on whether the value
30+
// falls below or above the range, respectively.
31+
// An EInvalidOpException exception is raised if the range is empty.
32+
function Constrain(const AValue: Integer): Integer;
33+
34+
// Checks if this range overlaps with ARange, i.e. the interection of the
35+
// ranges is non empty. Empty ranges cannot overlap with any range.
36+
function OverlapsWith(const ARange: TIntegerRange): Boolean;
37+
38+
// Checks if this range is immediately adjacent to ARange, with no overlap.
39+
// Empty ranges are never contiguous with other ranges or themselves.
40+
function IsContiguousWith(const ARange: TIntegerRange): Boolean;
41+
42+
// Checks if the set of all values in this range and ARange form a
43+
// continuous sequence. This implies that a range is continuous with itself.
44+
// Since adding an empty range to a non-empty range doesn't change the
45+
// non-empty range we define empty ranges to be continuous with any range.
46+
function IsContinuousWith(const ARange: TIntegerRange): Boolean;
47+
48+
// Checks if ranges A and B are the same
49+
class operator Equal(const A, B: TIntegerRange): Boolean;
50+
51+
// Checks if ranges A and B are not the same
52+
class operator NotEqual(const A, B: TIntegerRange): Boolean;
53+
54+
// Checks if range A is contained in, or is the same as, range B.
55+
// An empty range is deemed to be contained in any other range.
56+
class operator LessThanOrEqual(const A, B: TIntegerRange): Boolean;
57+
58+
// Checks if range A is contains, or is the same as, range B.
59+
// A non-empty range is never contained in an empty range.
60+
class operator GreaterThanOrEqual(const A, B: TIntegerRange): Boolean;
61+
62+
// Combine two ranges, A and B. The result is the smallest range that
63+
// contains both A and B.
64+
// If A and B are not continuous the resulting range will contain values
65+
// that were not in either A or B.
66+
// Combining any range either with itself or with an empty range is a no-op.
67+
class operator Add(const A, B: TIntegerRange): TIntegerRange;
68+
69+
// Returns a range that is the intersection of ranges A and B.
70+
// Returns an empty range if A and B do not overlap.
71+
class operator Multiply(const A, B: TIntegerRange): TIntegerRange;
72+
73+
// Checks if integer AValue is contained within range ARange.
74+
class operator In(const AValue: Integer; const ARange: TIntegerRange):
75+
Boolean;
76+
77+
// Implicitly casts ARange to a string. If ARange is non-empty the string
78+
// has format [X..Y], where X and Y are the lower and upper bounds of
79+
// ARange respectively. If ARange is empty then [] is returned.
80+
// This means that ARange can be assigned directly to a string.
81+
class operator Implicit(const ARange: TIntegerRange): string;
82+
83+
// Explicitly casts ARange to a string. If ARange is non-empty the string
84+
// has format [X..Y], where X and Y are the lower and upper bounds of
85+
// ARange respectively. If ARange is empty then [] is returned.
86+
// This means that ARange can be explicitly cast to a string using
87+
// string(ARange).
88+
class operator Explicit(const ARange: TIntegerRange): string;
89+
90+
// The lower bound of a non-empty range.
91+
// EInvalidOpException is raised if the property is read when the range is
92+
// empty.
93+
property LowerBound: Integer read GetLowerBound;
94+
95+
// The upper bound of a non-empty range.
96+
// EInvalidOpException is raised if the property is read when the range is
97+
// empty.
98+
property UpperBound: Integer read GetUpperBound;
99+
end;
100+
101+
class operator TIntegerRange.Add(const A, B: TIntegerRange): TIntegerRange;
102+
begin
103+
if A.IsEmpty then
104+
Exit(B);
105+
if B.IsEmpty then
106+
Exit(A);
107+
Result := TIntegerRange.Create(
108+
Math.Min(A.fLowerBound, B.fLowerBound),
109+
Math.Max(A.fUpperBound, B.fUpperBound)
110+
);
111+
end;
112+
113+
function TIntegerRange.Constrain(const AValue: Integer): Integer;
114+
begin
115+
if IsEmpty then
116+
raise Sysutils.EInvalidOpException.Create(
117+
'TIntegerRange.Constrain not valid for an empty range.'
118+
);
119+
Result := Math.EnsureRange(AValue, fLowerBound, fUpperBound);
120+
end;
121+
122+
constructor TIntegerRange.Create(const A, B: Integer);
123+
begin
124+
// Normalise range so that smallest parameter is the lower bound
125+
fLowerBound := Math.Min(A, B);
126+
fUpperBound := Math.Max(A, B);
127+
if fLowerBound = Low(Integer) then
128+
// This restriction is required to prevent the Length method's Cardinal
129+
// return value from wrapping around / overflowing
130+
raise SysUtils.EArgumentException.CreateFmt(
131+
'TIntegerRange.Create: Arguments must be greater than %d', [Low(Integer)]
132+
);
133+
end;
134+
135+
class function TIntegerRange.CreateEmpty: TIntegerRange;
136+
begin
137+
Result.fLowerBound := High(Integer);
138+
Result.fUpperBound := Low(Integer);
139+
end;
140+
141+
class operator TIntegerRange.Equal(const A, B: TIntegerRange): Boolean;
142+
begin
143+
if A.IsEmpty or B.IsEmpty then
144+
Exit(A.IsEmpty and B.IsEmpty);
145+
Result := (A.fLowerBound = B.fLowerBound) and (A.fUpperBound = B.fUpperBound);
146+
end;
147+
148+
class operator TIntegerRange.Explicit(const ARange: TIntegerRange): string;
149+
begin
150+
if ARange.IsEmpty then
151+
Exit('[]');
152+
Result := SysUtils.Format(
153+
'[%d..%d]', [ARange.fLowerBound, ARange.fUpperBound]
154+
);
155+
end;
156+
157+
function TIntegerRange.GetLowerBound: Integer;
158+
begin
159+
if IsEmpty then
160+
raise Sysutils.EInvalidOpException.Create(
161+
'TIntegerRange.LowerBound not valid for an empty range.'
162+
);
163+
Result := fLowerBound;
164+
end;
165+
166+
function TIntegerRange.GetUpperBound: Integer;
167+
begin
168+
if IsEmpty then
169+
raise Sysutils.EInvalidOpException.Create(
170+
'TIntegerRange.LowerBound not valid for an empty range.'
171+
);
172+
Result := fUpperBound;
173+
end;
174+
175+
class operator TIntegerRange.GreaterThanOrEqual(const A, B: TIntegerRange):
176+
Boolean;
177+
begin
178+
Result := B.IsSubrangeOf(A);
179+
end;
180+
181+
class operator TIntegerRange.Implicit(const ARange: TIntegerRange): string;
182+
begin
183+
Result := string(ARange); // calls Explicit cast operator
184+
end;
185+
186+
class operator TIntegerRange.In(const AValue: Integer;
187+
const ARange: TIntegerRange): Boolean;
188+
begin
189+
if ARange.IsEmpty then
190+
Exit(False);
191+
Result := (AValue >= ARange.fLowerBound) and (AValue <= ARange.fUpperBound);
192+
end;
193+
194+
function TIntegerRange.IsContiguousWith(const ARange: TIntegerRange): Boolean;
195+
begin
196+
if Self.IsEmpty or ARange.IsEmpty then
197+
Exit(False);
198+
Result := (Self + ARange).Length = (Self.Length + ARange.Length);
199+
end;
200+
201+
function TIntegerRange.IsContinuousWith(const ARange: TIntegerRange): Boolean;
202+
begin
203+
if Self.IsEmpty or ARange.IsEmpty then
204+
// Empty ranges are only continuous with other empty ranges
205+
Exit(True);
206+
Result := IsContiguousWith(ARange) or OverlapsWith(ARange);
207+
end;
208+
209+
function TIntegerRange.IsEmpty: Boolean;
210+
begin
211+
Result := fLowerBound > fUpperBound;
212+
end;
213+
214+
function TIntegerRange.IsSubrangeOf(const ARange: TIntegerRange): Boolean;
215+
begin
216+
if ARange.IsEmpty then
217+
Exit(Self.IsEmpty);
218+
Result := (Self.fLowerBound >= ARange.fLowerBound)
219+
and (Self.fUpperBound <= ARange.fUpperBound)
220+
or Self.IsEmpty
221+
end;
222+
223+
function TIntegerRange.Length: Cardinal;
224+
begin
225+
if IsEmpty then
226+
Exit(0);
227+
Result := fUpperBound - fLowerBound + 1
228+
end;
229+
230+
class operator TIntegerRange.LessThanOrEqual(const A, B: TIntegerRange):
231+
Boolean;
232+
begin
233+
Result := A.IsSubrangeOf(B);
234+
end;
235+
236+
class operator TIntegerRange.Multiply(const A, B: TIntegerRange): TIntegerRange;
237+
var
238+
Up, Lo: Integer;
239+
begin
240+
if A.IsEmpty or B.IsEmpty then
241+
Exit(TIntegerRange.CreateEmpty);
242+
Lo := Math.Max(A.fLowerBound, B.fLowerBound);
243+
Up := Math.Min(A.fUpperBound, B.fUpperBound);
244+
if Lo <= Up then
245+
Result := TIntegerRange.Create(Lo, Up)
246+
else
247+
Result := TIntegerRange.CreateEmpty;
248+
end;
249+
250+
class operator TIntegerRange.NotEqual(const A, B: TIntegerRange): Boolean;
251+
begin
252+
if A.IsEmpty or B.IsEmpty then
253+
Exit(A.IsEmpty <> B.IsEmpty);
254+
Result := (A.fLowerBound <> B.fLowerBound)
255+
or (A.fUpperBound <> B.fUpperBound);
256+
end;
257+
258+
function TIntegerRange.OverlapsWith(const ARange: TIntegerRange): Boolean;
259+
begin
260+
Result := not (Self * ARange).IsEmpty;
261+
end;

collection/structs.ini

+17-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ FPC=Y
5757
DescEx="<p>Constructs and returns a <var>TRange</var> record with bounds <var>A</var> and <var>B</var>.</p><p>The smaller of <var>A</var> and <var>B</var> is used as the lower bound and the larger as the upper bound. If both values are equal then the range will be empty.</p>"
5858
Depends=TRange
5959
SeeAlso=TRange
60-
SeeAlso=TRange,TRangeEx
60+
SeeAlso=TRange,TRangeEx,TIntegerRange
6161
Snip=580.dat
6262
Delphi7=Y
6363
Delphi2005Win32=Y
@@ -122,6 +122,20 @@ Delphi10S=Y
122122
Delphi12A=Y
123123
FPC=Y
124124

125+
[TIntegerRange]
126+
DisplayName=TIntegerRange
127+
DescEx="<p>An advanced record that encapsulates an integer range along with operations on it.</p><p>The range is immutable, so the constructor must be used to instantiate a non-empty range.</p>"
128+
Extra="<p><var>TIntegerRange</var> supports various operator overloads, which operate in a way similar to the Pascal <strong>set</strong> operators:</p><p><mono>=</mono> compares two ranges for equality.</p><p><mono>&lt;&gt;</mono> compares two ranges for inequality.</p><p><mono>&lt;=</mono> checks if the left operand is wholly contained in, or is equal to the right operand (c.f. Pascal subset operator).</p><p><mono>&gt;=</mono> checks if the left operand wholly contains, or is equal to, the right hand operand (c.f. Pascal proper superset operator).</p><p><mono>+</mono> creates the union of the two ranges specified by the operands, i.e. a new range that is the smallest range that contains both operands. For example, representing a range as <mono>[A..B]</mono>, <mono>[1..3] + [7..10] = [1..10]</mono> (c.f. Pascal union operator).</p><p><mono>*</mono> creates a new range that the largest range contained in both ranges specified by the operands, or an empty range if the operands do not overlap. For example, <mono>[1..3] * [2..7] = [2..3]</mono> while <mono>[1..3] * [6..9] is an empty range.</mono> (c.f. Pascal intersection operator).</p><p><strong><mono>in</mono></strong> checks if the integer specified in the left hand operand is contained in the range specified by the right hand operand (c.f. Pascal membership operator).</p>"
129+
Kind=class
130+
Units=SysUtils,Math
131+
SeeAlso=TRange,TRangeEx
132+
TestInfo=advanced
133+
AdvancedTest.Level=unit-tests
134+
AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs"
135+
Snip=706.dat
136+
DelphiXE=Y
137+
Delphi12A=Y
138+
125139
[TPointF]
126140
Kind=type
127141
DescEx="<p>Encapsulates a point with double precision floating point coordinates.</p>"
@@ -150,7 +164,7 @@ FPC=Y
150164
Kind=type
151165
DescEx="<p>Encapsulates the upper and lower bounds of a range of values.</p>"
152166
SeeAlso=Range
153-
SeeAlso=Range,TRangeEx
167+
SeeAlso=Range,TRangeEx,TIntegerRange
154168
Snip=579.dat
155169
Delphi7=Y
156170
Delphi2005Win32=Y
@@ -195,7 +209,7 @@ FPC=Y
195209
Kind=class
196210
DescEx="<p>Encapsulates a range of integers with a methods to test whether a value falls within the range and to adjust the value to fit.</p>"
197211
Units=Math
198-
SeeAlso=Range,TRange
212+
SeeAlso=Range,TRange,TIntegerRange
199213
Snip=578.dat
200214
Delphi2=N
201215
Delphi3=N
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
program TestCatStructsXE;
2+
{
3+
4+
Delphi DUnit Test Project
5+
-------------------------
6+
This project contains the DUnit test framework and the GUI/Console test runners.
7+
Add "CONSOLE_TESTRUNNER" to the conditional defines entry in the project options
8+
to use the console test runner. Otherwise the GUI test runner will be used by
9+
default.
10+
11+
}
12+
13+
{$IFDEF CONSOLE_TESTRUNNER}
14+
{$APPTYPE CONSOLE}
15+
{$ENDIF}
16+
17+
uses
18+
Forms,
19+
TestFramework,
20+
GUITestRunner,
21+
TextTestRunner,
22+
TestUStructCatSnippets in 'TestUStructCatSnippets.pas',
23+
UStructCatSnippets in 'UStructCatSnippets.pas';
24+
25+
{$R *.RES}
26+
27+
begin
28+
Application.Initialize;
29+
if IsConsole then
30+
with TextTestRunner.RunRegisteredTests do
31+
Free
32+
else
33+
GUITestRunner.RunRegisteredTests;
34+
end.
35+

0 commit comments

Comments
 (0)