Author: Lou Adler
I've heard of the Math unit that's included with the Developer level version of
Delphi. How do I use it?
Answer:
An Unsung Hero?
If you haven't heard of the Math unit, you're not alone. It's one of those units
that's kind of buried in the myriad directories under the Delphi directory. Also,
it's only included in the Delphi 2.0 Developer version and above. For those of you
who have Delphi 2.0 Developer, you can find the source code file in the
\\Borland\Delphi 2.0\Source\RTL\SYS directory.
This unit is one of those things I've heard a lot of people mention in the past but
unfortunately, I haven't seen many examples of using it. I'm not sure whether it's
because developers don't use it much or don't know about it. In any case, it's a
shame because what the Math unit has to offer could be helpful to people needing to
use mathematical functions in their code.
Not only are they helpful by their mere existence, but several of the member
functions are written in Assembly language that is optimized for the Pentium FPU,
so they're incredibly fast and efficient. For example, the procedure SinCos, which
is one of the procedures written in Assembly, will produce the Sin and Cos
simultaneously of an any angle faster than if you called the Sin and Cos functions
individually. I've seen it at work and it's amazing.
The Math unit includes several categories of mathematical functions you can
incorporate into your code. These include:
Trigonometric, Hyperbolic and Angle Conversion Functions
Logorithmic Functions
Exponential Functions
Some Miscellaneous Computing Functions
Several Statistical Functions
The Standard Set of Quattro Pro Financial Functions
Mind you, not all of the functions are coded in Assembly, but the mere fact that
they've already been written means you don't have to, so that's a real time saver.
Below are the function prototypes for the unit, so you can see what's in the file:
1
2 {-----------------------------------------------------------------------
3 Copyright (c) Borland International.
4
5 Most of the following trig and log routines map directly to Intel 80387 FPU
6 floating point machine instructions. Input domains, output ranges, and
7 error handling are determined largely by the FPU hardware.
8 Routines coded in assembler favor the Pentium FPU pipeline architecture.
9 -----------------------------------------------------------------------}
10
11 { Trigonometric functions }
12 function ArcCos(X: Extended): Extended; { IN: |X| <= 1 OUT: [0..PI] radians }
13 function ArcSin(X: Extended): Extended; { IN: |X| <= 1 OUT: [-PI/2..PI/2] radians
14 }
15
16 { ArcTan2 calculates ArcTan(Y/X), and returns an angle in the correct quadrant.
17 IN: |Y| < 2^64, |X| < 2^64, X <> 0 OUT: [-PI..PI] radians }
18 function ArcTan2(Y, X: Extended): Extended;
19
20 { SinCos is 2x faster than calling Sin and Cos separately for the same angle }
21 procedure SinCos(Theta: Extended; var Sin, Cos: Extended) register;
22 function Tan(X: Extended): Extended;
23 function Cotan(X: Extended): Extended; { 1 / tan(X), X <> 0 }
24 function Hypot(X, Y: Extended): Extended; { Sqrt(X**2 + Y**2) }
25
26 { Angle unit conversion routines }
27 function DegToRad(Degrees: Extended): Extended; { Radians := Degrees * PI / 180}
28 function RadToDeg(Radians: Extended): Extended; { Degrees := Radians * 180 / PI }
29 function GradToRad(Grads: Extended): Extended; { Radians := Grads * PI / 200 }
30 function RadToGrad(Radians: Extended): Extended; { Grads := Radians * 200 / PI }
31 function CycleToRad(Cycles: Extended): Extended; { Radians := Cycles * 2PI }
32 function RadToCycle(Radians: Extended): Extended;{ Cycles := Radians / 2PI }
33
34 { Hyperbolic functions and inverses }
35 function Cosh(X: Extended): Extended;
36 function Sinh(X: Extended): Extended;
37 function Tanh(X: Extended): Extended;
38 function ArcCosh(X: Extended): Extended; { IN: X >= 1 }
39 function ArcSinh(X: Extended): Extended;
40 function ArcTanh(X: Extended): Extended; { IN: |X| <= 1 }
41
42 { Logorithmic functions }
43 function LnXP1(X: Extended): Extended; { Ln(X + 1), accurate for X near zero }
44 function Log10(X: Extended): Extended; { Log base 10 of X}
45 function Log2(X: Extended): Extended; { Log base 2 of X }
46 function LogN(Base, X: Extended): Extended; { Log base N of X }
47
48 { Exponential functions }
49
50 { IntPower: Raise base to an integral power. Fast. }
51 function IntPower(Base: Extended; Exponent: Integer): Extended register;
52
53 { Power: Raise base to any power.
54 For fractional exponents, or exponents > MaxInt, base must be > 0. }
55 function Power(Base, Exponent: Extended): Extended;
56
57
58 { Miscellaneous Routines }
59
60 { Frexp: Separates the mantissa and exponent of X. }
61 procedure Frexp(X: Extended; var Mantissa: Extended; var Exponent: Integer)
62 register;
63
64 { Ldexp: returns X*2**P }
65 function Ldexp(X: Extended; P: Integer): Extended register;
66
67 { Ceil: Smallest integer >= X, |X| < MaxInt }
68 function Ceil(X: Extended):Integer;
69
70 { Floor: Largest integer <= X, |X| < MaxInt }
71 function Floor(X: Extended): Integer;
72
73 { Poly: Evaluates a uniform polynomial of one variable at value X.
74 The coefficients are ordered in increasing powers of X:
75 Coefficients[0] + Coefficients[1]*X + ... + Coefficients[N]*(X**N) }
76 function Poly(X: Extended; const Coefficients: array of Double): Extended;
77
78 {-----------------------------------------------------------------------
79 Statistical functions.
80
81 Common commercial spreadsheet macro names for these statistical and
82 financial functions are given in the comments preceding each function.
83 -----------------------------------------------------------------------}
84
85 { Mean: Arithmetic average of values. (AVG): SUM / N }
86 function Mean(const Data: array of Double): Extended;
87
88 { Sum: Sum of values. (SUM) }
89 function Sum(const Data: array of Double): Extended register;
90 function SumOfSquares(const Data: array of Double): Extended;
91 procedure SumsAndSquares(const Data: array of Double;
92 var Sum, SumOfSquares: Extended) register;
93
94 { MinValue: Returns the smallest signed value in the data array (MIN) }
95 function MinValue(const Data: array of Double): Double;
96
97 { MaxValue: Returns the largest signed value in the data array (MAX) }
98 function MaxValue(const Data: array of Double): Double;
99
100 { Standard Deviation (STD): Sqrt(Variance). aka Sample Standard Deviation }
101 function StdDev(const Data: array of Double): Extended;
102
103 { MeanAndStdDev calculates Mean and StdDev in one pass, which is 2x faster than
104 calculating them separately. Less accurate when the mean is very large
105 (> 10e7) or the variance is very small. }
106 procedure MeanAndStdDev(const Data: array of Double; var Mean, StdDev: Extended);
107
108 { Population Standard Deviation (STDP): Sqrt(PopnVariance).
109 Used in some business and financial calculations. }
110 function PopnStdDev(const Data: array of Double): Extended;
111
112 { Variance (VARS): TotalVariance / (N-1). aka Sample Variance }
113 function Variance(const Data: array of Double): Extended;
114
115 { Population Variance (VAR or VARP): TotalVariance/ N }
116 function PopnVariance(const Data: array of Double): Extended;
117
118 { Total Variance: SUM(i=1,N)[(X(i) - Mean)**2] }
119 function TotalVariance(const Data: array of Double): Extended;
120
121 { Norm: The Euclidean L2-norm. Sqrt(SumOfSquares) }
122 function Norm(const Data: array of Double): Extended;
123
124 { MomentSkewKurtosis: Calculates the core factors of statistical analysis:
125 the first four moments plus the coefficients of skewness and kurtosis.
126 M1 is the Mean. M2 is the Variance.
127 Skew reflects symmetry of distribution: M3 / (M2**(3/2))
128 Kurtosis reflects flatness of distribution: M4 / Sqr(M2) }
129
130 procedure MomentSkewKurtosis(const Data: array of Double;
131 var M1, M2, M3, M4, Skew, Kurtosis: Extended);
132
133 { RandG produces random numbers with Gaussian distribution about the mean.
134 Useful for simulating data with sampling errors. }
135 function RandG(Mean, StdDev: Extended): Extended;
136
137 {-----------------------------------------------------------------------
138 Financial functions. Standard set from Quattro Pro.
139
140 Parameter conventions:
141
142 From the point of view of A, amounts received by A are positive and
143 amounts disbursed by A are negative (e.g. a borrower's loan repayments
144 are regarded by the borrower as negative).
145
146 Interest rates are per payment period. 11% annual percentage rate on a
147 loan with 12 payments per year would be (11 / 100) / 12 = 0.00916667
148
149 -----------------------------------------------------------------------}
150
151 type
152 TPaymentTime = (ptEndOfPeriod, ptStartOfPeriod);
153
154 { Double Declining Balance (DDB) }
155 function DoubleDecliningBalance(Cost, Salvage: Extended;
156 Life, Period: Integer): Extended;
157
158 { Future Value (FVAL) }
159 function FutureValue(Rate: Extended; NPeriods: Integer; Payment, PresentValue:
160 Extended; PaymentTime: TPaymentTime): Extended;
161
162 { Interest Payment (IPAYMT) }
163 function InterestPayment(Rate: Extended; Period, NPeriods: Integer; PresentValue,
164 FutureValue: Extended; PaymentTime: TPaymentTime): Extended;
165
166 { Interest Rate (IRATE) }
167 function InterestRate(NPeriods: Integer;
168 Payment, PresentValue, FutureValue: Extended; PaymentTime: TPaymentTime):
169 Extended;
170
171 { Internal Rate of Return. (IRR) Needs array of cash flows. }
172 function InternalRateOfReturn(Guess: Extended;
173 const CashFlows: array of Double): Extended;
174
175 { Number of Periods (NPER) }
176 function NumberOfPeriods(Rate, Payment, PresentValue, FutureValue: Extended;
177 PaymentTime: TPaymentTime): Extended;
178
179 { Net Present Value. (NPV) Needs array of cash flows. }
180 function NetPresentValue(Rate: Extended; const CashFlows: array of Double;
181 PaymentTime: TPaymentTime): Extended;
182
183 { Payment (PAYMT) }
184 function Payment(Rate: Extended; NPeriods: Integer;
185 PresentValue, FutureValue: Extended; PaymentTime: TPaymentTime): Extended;
186
187 { Period Payment (PPAYMT) }
188 function PeriodPayment(Rate: Extended; Period, NPeriods: Integer;
189 PresentValue, FutureValue: Extended; PaymentTime: TPaymentTime): Extended;
190
191 { Present Value (PVAL) }
192 function PresentValue(Rate: Extended; NPeriods: Integer;
193 Payment, FutureValue: Extended; PaymentTime: TPaymentTime): Extended;
194
195 { Straight Line depreciation (SLN) }
196 function SLNDepreciation(Cost, Salvage: Extended; Life: Integer): Extended;
197
198 { Sum-of-Years-Digits depreciation (SYD) }
199 function SYDDepreciation(Cost, Salvage: Extended; Life, Period: Integer):
200 Extended;
Clearing Things Up: Usage
Whew! That's a lot of information to digest! I listed it here to impress upon you
just how much there is. For those of you creating financial applications, the
financial functions will come in handy (I sure wish I had these functions available
when I was writing financial software).
Listing the functions is one thing - actually using them is another. As you can
see, most of the input parameters require an Extended numeric type, which is a
10-byte number ranging from 3.4 * 10e-4932 to 1.1 * 10e4932 in scientific notation.
In other words, you can have incredibly huge numbers as input values.
Take a moment to look at the statistical functionsBOOK1. Notice anything odd about
almost all of the functions' input parameters? Most of them take a constant open
array of double! This implies you can pass any size array as an input parameter,
but it must be passed as a constant array, which means that you have to pass the
array in the form of (1, 2, 3, 4, 5, 6 ..). That's not so difficult with small
known sets of numbers; just hard code them in. But arrays in Pascal are typically
of the variable type, where you define a finite number of elements, then fill in
the element values. This poses a bit of a problem. Fortunately, there's a solution.
Buried in the System unit is a function called Slice: function Slice(var A: array;
Count: Integer): array; Essentially, what Slice does is take a certain number of
elements from an array, starting at the beginning, and passes the slice of the
array as an open array parameter, fooling the compiler into thinking a constant
array is being passed. This means that you can pass the entire array or smaller
subset. In fact, Slice can only be used within the context of being passed as an
open array parameter. Using it outside of this context will create a compiler
error. What a convenient function! So, we can define a variable type array as we're
used to in Delphi, fill it up, put it into Slice, which is then used in one of the
functions. For example: MyExtendedNumber := Mean(Slice(MyArray, NumElementsPassed));
At this point, you're probably thinking this is pretty incredible stuff. But
there's one thing that still bothers me about it: The place where the statistical
functions would be most useful is on columnar calculations on tables.
Unfortunately, you never know how many records are in a table until runtime.
Granted, depending upon the amount of RAM in your system, you could make an
incredibly large array of let's say 100K elements, fill it up to the record count
of your table, then apply Slice to grab only those you need. However, that's pretty
inefficient. Also, in my immediate experience, many of my tables have well over
100K records, which means I'd have to hard code an even greater upper limit. But
there will also be tables that have far fewer records than 100K - more like 10K. So
the idea then is to strike a balance. No thanks!
Doing the DynArray Thing
So where am I if I can't accept defining a huge array, or making some sort of size
compromise? I guess I need to create a variable sized array whose size can be
defined at runtime.
Wait a minute! You're not supposed to be able to do that in Delphi!
You can, but it takes some pointers to be able to pull it off. For an in-depth
discussion of the technique, I'm going to point you to an article on the enquiry
site entitled Runtime Resizeable Arrays, which will show you how to create an array
that has an element count you don't know about until runtime. I highly suggest
reading the article before continuing, if you're not familiar with the technique.
What gives you the ability to create a dynamic array is a function like the
following (this in the article):
201 type
202 TResizeArr = array[0..0] of string;
203 PResizeArr = ^TResizeArr;
204 ...
205
206 {============================================================================
207 Procedure which defines the dynarray. Note that the THandle and Pointer to
208 the array are passed by reference. This is so that they may defined outside
209 the scope of this proc.
210 ============================================================================}
211
212 procedure DefineDynArray(var h: THandle; {Handle to mem pointer}
213 NumElements: LongInt; {Number of items in array}
214 var PArr: PResizeArr); {Pointer to array struct}
215 begin
216 {Allocate Windows Global Heap memory}
217 h := GlobalAlloc(GMEM_FIXED, NumElements * sizeof(TResizeArr));
218 PArr := GlobalLock(h);
219 end;
Note that you can just as easily replace the array of String with an array of
Double. In any case, the gist of what the procedure does is allocate memory for the
number of elements that you want to have in your array (TResizeArr), then locks
that area of the heap and assigns it to an array pointer (PResizeArr). To load
values into the array, you de-reference the pointer as follows: MyPointerArray^[0]
:= 1234.1234;
To pass the entire array into the Mean function as above, all we have to do is
de-reference the entire array as follows: MyExtendedNumber :=
Mean(Slice(MyPointerArray^, NumElementsPassed));
Putting It All Together
I mentioned above that the best place to employ the statistical functions is in
performing statistics on columnar data in a table. The unit code below provides a
simple example of loading a DynArray with columnar data, then performing the Mean
function on the loaded array. Note the table that I used had about 80K records in
it.
220 unit parrmain;
221
222 interface
223
224 uses
225 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
226 DB, DBTables, ComCtrls, Math, StdCtrls, ds_stats;
227
228 type
229 TResizeArr = array[0..0] of Double;
230 PResizeArr = ^TResizeArr;
231 TForm1 = class(TForm)
232 StatusBar1: TStatusBar;
233 Button1: TButton;
234 Table1: TTable;
235 procedure Button1Click(Sender: TObject);
236 private
237 { Private declarations }
238 PA: PResizeArr;
239 Hndl: THandle;
240 procedure DefineDynArray(var H: THandle;
241 NumElem: LongInt;
242 var PArr: PResizeArr);
243 procedure FillArray;
244 procedure SaveScreen;
245 public
246 { Public declarations }
247
248 end;
249
250 var
251 Form1: TForm1;
252
253 implementation
254
255 {$R *.DFM}
256
257 procedure TForm1.DefineDynArray(var H: THandle;
258 NumElem: LongInt;
259 var PArr: PResizeArr);
260 begin
261 H := GlobalAlloc(GMEM_FIXED, NumElem * SizeOf(TResizeArr));
262 PArr := GlobalLock(H);
263 end;
264
265 procedure TForm1.LoadArray;
266 var
267 tbl: TTable;
268 recs: LongInt;
269 begin
270 tbl := TTable.Create(Application); //Create a TTable instance
271 with tbl do
272 begin //and set properties
273 Active := False;
274 DatabaseName := 'Primary';
275 TableName := 'Test';
276 TableType := ttParadox;
277 Open;
278
279 recs := RecordCount; //Get number of records in table
280
281 DefineDynArray(Hndl, recs, PA); //Now, define our dynarray
282 recs := 0; //Reset recs for reuse
283
284 StatusBar1.SimpleText := 'Now filling array';
285
286 while not EOF do
287 begin
288 Application.ProcessMessages; //allow background processing
289 try
290 PA^[recs] := FieldByName('Charge').AsFloat;
291 StatusBar1.SimpleText := 'Grabbed value of: ' + FloatToStr(PA^[recs]);
292 StatusBar1.Invalidate;
293 Inc(recs);
294 Next;
295 except
296 GlobalUnlock(Hndl);
297 GlobalFree(Hndl);
298 Exit;
299 end;
300 end;
301
302 //Pop up a message to show what was calculated.
303 ShowMessage(FloatToStr(Mean(Slice(PA^, RecordCount))));
304 //pass the array using Slice
305 GlobalUnlock(Hndl); //Unlock and Free memory and TTable instance.
306 GlobalFree(Hndl);
307 tbl.Free;
308 end;
309 end;
310
311 procedure TForm1.Button1Click(Sender: TObject);
312 begin
313 LoadArray;
314 end;
315
316 end.
You could get pretty complex with this by creating a component that encapsulates
the statistical functions and grabs data off a table. Using the principles of the
code above, it shouldn't be too hard to do. Follow the code; better yet, try it
out, supply your own values, and see what you come up with.
We covered a lot of ground here. I wasn't happy to tell you merely about the Math
unit and all the wonderful routines it contains; I wanted to show you a way to
employ a major portion of it in as flexible a way as possible.
In my opinion, it's not enough just to know about something in programming; you have to know how to use it. With the material I've presented, you should be able to employ the functions of the Math unit in very little time.
|