I'm looking for a function somewhere in Delphi XE2 similar to Inc() which allows me to add/subtract a number of degrees from a current number of degrees and result in the new degrees. For example, if I have a point currently at 5 degrees around a circle, and I want to subtract 10, I should not get -5 degrees, but rather 355 (360 - 5). Same as adding past 360 - it should go back to 0 when it reaches 360.
Is there anything like this already in Delphi so I don't have to re-write it? Perhaps in the Math unit?
uses
System.SysUtils,Math;
Function WrapAngle( angle : Double) : Double;
Const
modAngle : Double = 360.0;
begin
Result := angle - modAngle*Floor(angle/modAngle);
end;
begin
WriteLn(FloatToStr(WrapAngle(-5)));
WriteLn(FloatToStr(WrapAngle(5-720)));
WriteLn(FloatToStr(WrapAngle(360)));
ReadLn;
end.
Produces result:
355
5
0
Update:
As #Giel found, in XE3 there is a new function DegNormalize() which does the job. Even about 25% faster. The trick is to replace the Floor() call with an Int() instead, and if the result is negative, add modAngle to the result.
function WrapAngle(Value: Integer): Integer;
begin
Result := Value mod 360;
if Result < 0 then
Inc(Result, 360);
end;
The code I use to perform this task is:
function PosFrac(x: Double): Double;
(* PosFrac(1.2)=0.2 and PosFrac(-1.2)=0.8. *)
begin
Result := Frac(x); (* Frac(x)=x-Int(x) *)
if Result<0.0 then begin
Result := 1.0+Result;
end;
end;
function ModR(const x, y: Double): Double;
(* ModR(1.2,1)=0.2 and ModR(-1.2,1)=0.8 *)
var
absy: Double;
begin
if y=0.0 then begin
Result := 0.0;
end else begin
absy := abs(y);
Result := PosFrac(x/absy)*absy;
end;
end;
function Mod360(const x: Double): Double;
begin
Result := ModR(x, 360.0);
end;
This code will bring all angles into the range 0 to 360. For example:
Writeln(Round(Mod360(5-10)));
Writeln(Round(Mod360(5-360)));
Writeln(Round(Mod360(5-720)));
Writeln(Round(Mod360(5+720)));
outputs:
355
5
5
5
I don't know any, but I'd prefer using a more general solution anyway ...
Procedure IncOverFlow(var Value:Double;Difference:Double;Limit:Double=360);
begin
Value := Value + Difference;
While Value < 0 do Value := Value + Limit;
While Value >= Limit do Value := Value -Limit;
end;
procedure WrapAngle(var Degs: Integer);
begin
Degs := Degs mod 360;
if Degs < 0 then
Inc(Degs, 360);
end;
Related
The code below draws a logarithmic grid with DrawGrid(). It seems the vertical lines are ok.
When I use the function SetPositionHzValue() the resulting position seems ok (it uses the same logic as the DrawGrid() and seems to match the grid).
But how to convert this 0 - 1.0 normalized value, that uses the display width linearly, to an actual Hz value? Why is the function GetPositionsHzValue() wrong?
To complicate things, the display has a start frequency (20 Hz in this case) and an end frequency (44100 Hz in this case).
procedure TAudioBezierCurves.DrawGrid(Bitmap32: TBitmap32);
var
GridPosition: Integer;
GridPositionF: Double;
i: Integer;
Base: Double;
LogOffsetValue: Double;
LogMaxValue: Double;
begin
GridPosition := 0;
Base := 1;
if GridFrequencyMin = 0 then begin
LogOffsetValue := 0;
end else begin
LogOffsetValue := Log10(GridFrequencyMin);
end;
LogMaxValue := Log10(GridFrequencyMax) - LogOffsetValue;
repeat
for i := 2 to 10 do begin
if Base * i < GridFrequencyMin then begin
Continue;
end;
//* This gives the % value relative to the total scale
GridPositionF := (Log10(Base * i) - LogOffsetValue) / LogMaxValue;
GridPositionF := GridPositionF * Bitmap32.Width;
GridPosition := Trunc(GridPositionF);
Bitmap32.VertLineS(GridPosition, 0, Bitmap32.Height - 1, GridColor);
end;
Base := Base * 10;
until GridPosition > Bitmap32.Width;
end;
procedure TAudioBezierCurve.SetPositionHzValue(AValue: Double);
var
LogOffsetValue: Double;
LogMaxValue: Double;
begin
if AValue = 0 then begin
Self.Position := 0;
end else begin
if Parent.GridFrequencyMin = 0 then begin
LogOffsetValue := 0;
end else begin
LogOffsetValue := Log10(Parent.GridFrequencyMin);
end;
LogMaxValue := Log10(Parent.GridFrequencyMax) - LogOffsetValue;
//* This gives the % value relative to the total scale
AValue := (Log10(AValue) - LogOffsetValue) / LogMaxValue;
Self.Position := AValue;
end;
end;
function TAudioBezierCurve.GetPositionsHzValue: Double;
var
AValue: Double;
begin
AValue := Self.Position;
AValue := Power(AValue, 2);
Result := AValue * (Parent.GridFrequencyMax);
Result := Result - (AValue * Parent.GridFrequencyMin) + Parent.GridFrequencyMin;
end;
EDIT: Ok, almost ok now. So it seems the correct function is:
AValue := Power(AValue, 10);
But still not perfect. Changing the display range to min. 0 to 44100, for simplicity, results that setting to the upper value 44100 is ok, the function GetPositionsHzValue() report 41100. But calling setting the position value to 20, GetPositionsHzValue() reports 0.
Trying to decrement the position all is fine until 44085, but 44084 value is reported as 44085 and this difference increases with smaller values. Going from lower values, it's 0 until 39, 40 results 1.
In function GetPositionsHzValue, line "AValue := Power(AValue, 2);" where does the value of "AValue" come from?
Maybe you should do something like you did in "SetPositionHzValue(AValue: Double);". AValue should be a parameter, not a local variable.
Found the solution, it should be:
function TAudioBezierCurve.GetPositionsHzValue: Double;
var
AValue: Double;
begin
AValue := Self.Position;
AValue := AValue * Log10(Parent.GridFrequencyMax) + (Log10(Parent.GridFrequencyMin) * (1 - AValue)); //* Results "min." at 0
Result := Power(10, AValue);
end;
If possible, I would like to avoid converting a Currency to Extended (and possible losing precision) in code similar to the following:
function CurrencyToNumeric(aCurrency: Currency; aScale: Integer): Int64;
const
scales: array [-{18}5..-1] of int64 = (100000, 10000, 1000, 100, 10);
var
aCurrencyAsInt64: Int64 absolute aCurrency;
begin
if aScale = -4 then
Result := aCurrencyAsInt64
else
Result := Round(aCurrency * scales[aScale]); // currency -> extended -> integer
end;
Is that possible?
I believe that you are looking for a function like this:
function CurrencyToNumeric(aCurrency: Currency; aScale: Integer): int64;
var
aCurrencyAsInt64: int64 absolute aCurrency;
i, factor, rem: Integer;
begin
if aScale <= -4 then begin
factor := 1;
for i := -4 downto aScale+1 do begin
factor := factor * 10;
end;
Result := aCurrencyAsInt64 * factor;
end else begin
factor := 1;
for i := -4 to aScale-1 do begin
factor := factor * 10;
end;
Result := aCurrencyAsInt64 div factor;
rem := aCurrencyAsInt64 mod factor;
if rem>=factor div 2 then begin
inc(Result);
end;
end;
end;
This part of the code
if rem>=factor div 2 then begin
inc(Result);
end;
implements the rounding policy. You may very well wish to make a different choice. Modify this code to do so, it should be obvious how to go about that.
However, I am also not convinced that the version in the question is broken. Do you have any example input where it fails? On the other hand, avoiding converting to binary floating point for a fixed point decimal type does feel sensible. Now, if only Embarcadero had implemented this darn type without resorting to using floating point operations.
Thanks to David's answer, I ended up with following implementation, which is not only float-free but also faster than function from the question.
function CurrencyToNumeric(Value: Currency; Scale: Integer): Int64;
const
factors: array [-4..-1] of Int64 = (10000, 1000, 100, 10);
var
factor: Integer;
ValueAsInt64: Int64 absolute Value;
begin
if Scale = -4 then
Result := ValueAsInt64
else if Scale < -4 then
Result := ValueAsInt64 * factors[4 + Scale]
else begin
factor := factors[-(4 + Scale)];
Result := ValueAsInt64 div factor;
if ValueAsInt64 mod factor >= factor div 2 then Inc(Result);
end;
end;
I have the following function which I'm led to believe should round time to nearest 15 minutes.
function TdmData.RoundTime(T: TTime): TTime;
var h, m, s, ms : Word;
begin
DecodeTime(T, h, m, s, ms);
m := (m div 15) * 15;
s := 0;
Result := EncodeTime(h, m, s, ms);
end;
To test the function I have put a tbutton and a tedit on a form and at the click of the button I do:
begin
Edit1.Text := RoundTime('12:08:27');
end;
I get an error when compiling : 'Incompatible types TTime and string'
Any help with this would be great.
Thanks,
The error which causes the compilation failure is that you are passing a string to a function which needs a TTime as a parameter.
Once this is fixed, Edit1.Text needs a string type but your function returns TTime.
Using StrToTime and TimeToStr you can obtain the desired conversion from and to a string type.
Your function can be called like this:
begin
Edit1.Text := TimeToStr(RoundTime(StrToTime('12:08:27'));
end;
Stealing the gabr user's answer - In Delphi: How do I round a TDateTime to closest second, minute, five-minute etc? - you can obtain a date rounded to an arbitrary nearest value assigned to the interval parameter:
function RoundToNearest(time, interval: TDateTime): TDateTime;
var
time_sec, int_sec, rounded_sec: int64;
begin
time_sec := Round(time * SecsPerDay);
int_sec := Round(interval * SecsPerDay);
rounded_sec := (time_sec div int_sec) * int_sec;
if ((rounded_sec + int_sec - time_sec) - (time_sec - rounded_sec)) > 0 then
rounded_sec := rounded_sec + time_sec + int_sec;
Result := rounded_sec / SecsPerDay;
end;
begin
Edit1.Text := TimeToStr(RoundToNearest(StrToTime('12:08:27'), StrToTime('0:0:15')));
end;
How can i find out which number is closer? say my value is "1" and i have two var, A:= 1.6 and b:=1.001
currently looking at a few numbers and taking a 0.1% +/- difference and a +/- 0.6 difference.. i just need to see which answer is closer to the starting value.. code so far..
Also nothing to big, the code is just to stop me from doing them all manually :D
procedure TForm1.Button1Click(Sender: TObject);
var
winlimit,test6high,test6low,test6,test1high,test1low,test1,value: double;
begin
value := 1.0;
while value < 1048567 do
begin
test6high := value + 0.6 ;
test6low := value - 0.6 ;
test1high := (-0.1 * value)/100;
test1high := value - test1high;
test1low := (0.1 * value)/100;
test1low := value - test1low;
memo1.Lines.Add('value is '+floattostr(value)+': 1% High:'+floattostr(Test1high)+' 1% Low:'+floattostr(Test1low));
memo1.Lines.Add('value is '+floattostr(value)+': 0.6 +/- '+floattostr(Test6high)+' 0.6 Low:'+floattostr(Test6low));
memo1.Lines.Add(' ');
value := value*2;
end
end;
I think you mean a function like this:
function ClosestTo(const Target, Value1, Value2: Double): Double;
begin
if abs(Target-Value1)<abs(Target-Value2) then
Result := Value1
else
Result := Value2;
end;
If you use IfThen from the Math unit you can write it more concisely:
function ClosestTo(const Target, Value1, Value2: Double): Double;
begin
Result := IfThen(abs(Target-Value1)<abs(Target-Value2), Value1, Value2);
end;
So, I'm trying to get the angle between two TPoints in Delphi, and it turns out to be harder then what I expected. The result I'm getting I can't explain (seems to be some problem with "to degrees"-part, or ArcTan2 does not return a sum in the form I expected.
- Delpi-v7:
function Modulo(x,y:Extended): Extended;
var d: Extended;
begin
d := x / y;
Result := (d - floor(d)) * y;
end;
function Degrees(Rads: Extended): Extended;
begin
Result := Rads*(180/Pi);
end;
function GetPointAngle(P1, P2: TPoint): Extended;
begin
Result := Modulo(Degrees(ArcTan2(-(P1.Y - P2.Y), P1.X - P2.X)) - 90, 360);
end;
Yet, when I port the code to Python, or test it in another Pascal-variant, the above works. But now, it seems to return a sum that's static (not changing if I "move" the second TPoint).
In case your wondering; I created "modulo"-function simply because the divide-operator used in the "mod"-operator rounds to 0, and not down (so negative numbers don't work).
Edit: I noted that the value (angle) returned from GetPointAngle() increases when p gets further away from the other point c (and vice versa), even tho the TPoint (p) is dragged along the X-axis of the second TPoint (c).
EDIT:
You guys have outdone your self, I've looked over most of the answers, and it seems to be hard to choose best answer! And since you guys wrote everything with such detail, I will go trough everything with the same detail :-)
Also: what I did not share in my initial post, is that my function is being exported as a DLL to be reached from another pascal-interpretor (which is delphi-compatible).
Solution at last (changed):
GetPointAngle(P1, P2: TPoint) To: GetPointAngle(const P1, P2: TPoint)
^ I don't understand the need of declaring constants...
I assume you want to calculate the angle relative to the X-axis of the line which is formed between those two points.
For this situation, the following formula applies:
Tan(a) = (P2.Y - P1.Y) / (P2.X - P1.X)
Which translates to:
a = ArcTan((P2.Y - P1.Y) / (P2.X - P1.X))
When the two points have the same X coordinate, this will obviously result in a EDivByZero exception, so you have to take care of that yourself. Furthermore, ArcTan results in an angle within the range 0°..90° (i.e. 0..π/2) and thus disregards the correct quadrant, while ArcTan2 results in an angle within -180°..180°. Add 360° to the result to convert a negative angle to positive:
function AngleOfLine(const P1, P2: TPoint): Double;
begin
if P2.X = P1.X then
if P2.Y > P1.Y then
Result := 90
else
Result := 270
else
Result := RadToDeg(ArcTan2(P2.Y - P1.Y, P2.X - P1.X));
if Result < 0 then
Result := Result + 360;
end;
Which results in:
A := AngleOfLine(Point(10, 10), Point(20, 10)); // 0
A := AngleOfLine(Point(10, 10), Point(20, 20)); // 45
A := AngleOfLine(Point(10, 10), Point(10, 20)); // 90
A := AngleOfLine(Point(10, 10), Point(0, 20)); // 135
A := AngleOfLine(Point(10, 10), Point(0, 10)); // 180
A := AngleOfLine(Point(10, 10), Point(0, 0)); // 225
A := AngleOfLine(Point(10, 10), Point(10, 0)); // 270
A := AngleOfLine(Point(10, 10), Point(20, 0)); // 315
Now, this is relative to the world coordinate system which has its positive Y-axis pointed upwards by default. If you want to convert the result to the device coordinate system wherein the positive Y-axis points downwards, then subtract the result from 360°:
Result := 360 - Result;
Update:
It seems ArcTan2 dóes take care of division by zero, (even in D7 inspite of the documentation) so the routine becomes much simpler:
function AngleOfLine(const P1, P2: TPoint): Double;
begin
Result := RadToDeg(ArcTan2((P2.Y - P1.Y),(P2.X - P1.X)));
if Result < 0 then
Result := Result + 360;
end;
Edit:
I noted that the value returned from GetPointAngle() increases when p gets furter away from the other point c (and vice versa).
That depends. Looking at the diagram above, if the second point moves further along the x-axis, the angle decreases. If the second point moves further along the y-axis, the angle increases. Of course, this depends on which quadrant both points are in.
Furthermore, your code negates the first parameter of ArcTan2 and subtracts another 90° from the result. I do not know what you mean by that and whether it is intentional, but it could be the source of unexpected results.
I presume what you are looking for is the angle between two vectors. That is θ in this diagram:
The algebraic dot product can be expressed geometrically as <v1,v2> = |v1||v2|cos θ. This can be rearranged to find θ = cos-1 <v1,v2>/(|v1||v2|).
function DotProduct(const v1, v2: TPoint): Integer;
begin
Result := v1.X*v2.X + v1.Y*v2.Y;
end;
function Magnitude(const v: TPoint): Double;
begin
Result := Sqrt(Sqr(v.X)+Sqr(v.Y));
end;
function AngleBetweenVectors(const v1, v2: TPoint): Double;
var
Magv1, Magv2: Double;
begin
Magv1 := Magnitude(v1);
Magv2 := Magnitude(v2);
if abs(Magv1*Magv2)=0.0 then
Result := 0.0
else
Result := ArcCos(EnsureRange(DotProduct(v1,v2)/(Magv1*Magv2), -1.0, 1.0));
end;
That returns an angle in radians. You can convert that into degrees using RadToDeg() from the Math unit.
Now, the other way to interpret your problem is that you want to take two points and form the line between then. And then find the angle between that line and the horizontal, say. As described by this diagram:
The can still be expressed as the angle between two vectors. The first vector is p2-p1 and the other is a vector in the horizontal direction, (0, 1). Feed those two into AngleBetweenVectors and you have your answer. If you want to measure angle to vertical, then you can use the same idea.
Hopefully there's enough here for you to solve the problem, whatever it actually is.
Following code returns same results with Delphi 7 and FPC 2.7.1 and it seems correct.
So main question is: what we are expecting and what we are having?
program Project2;
{$APPTYPE CONSOLE}
uses
Math;
{.$define speed}
function CalcAngle(const lx, ly: extended): extended; {$ifdef speed} inline; {$endif}
begin
Result := RadToDeg(ArcTan2(ly, lx));
end;
function Modulo(x, y: extended): extended; {$ifdef speed} inline; {$endif}
var
d: extended;
begin
d := x / y;
Result := (d - floor(d)) * y;
end;
function Degrees(Rads: Extended): Extended;
begin
Result := Rads*(180/Pi);
end;
function Modulo2(x: extended): extended; {$ifdef speed} inline; {$endif}
begin
if x < 0 then
Result := 360 + x
else
Result := x;
end;
function GetPointAngle(const lx, ly: integer): Extended;
begin
Result := Modulo(Degrees(ArcTan2(ly, lx)) - 90, 360);
end;
procedure OutTest(const lx, ly: extended);
var
a: extended;
begin
a := CalcAngle(lx, ly);
Writeln(
a: 10: 4,
Modulo(a - 90, 360):10:4,
GetPointAngle(round(lx), round(ly)):10:4);
end;
begin
OutTest(2, 0);
OutTest(0, 2);
OutTest(-2, 2);
OutTest(-2, -2);
OutTest(2, 3);
OutTest(100, 2);
Readln;
end.